From 17403f5bf995f2b65f0e6b60a6dc466369b0ffab Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Mon, 30 Mar 2026 16:01:50 +0200 Subject: [PATCH 1/2] http-service: accept single-stage WS tiles in staged requests --- libs/http-service/src/tiles-ws-controller.cpp | 41 ++++++++++++++++--- test/unit/test-http-datasource.cpp | 19 +++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/libs/http-service/src/tiles-ws-controller.cpp b/libs/http-service/src/tiles-ws-controller.cpp index c402f741..f9b05c77 100644 --- a/libs/http-service/src/tiles-ws-controller.cpp +++ b/libs/http-service/src/tiles-ws-controller.cpp @@ -823,6 +823,36 @@ class TilesWsSession : public std::enable_shared_from_this } } + /// Match one backend-produced tile key against the currently desired request set. + [[nodiscard]] std::optional matchDesiredTileKeyLocked( + MapTileKey key, + uint32_t advertisedStages) const + { + auto requestedTileKey = makeCanonicalRequestedTileKey(std::move(key)); + if (desiredTileKeys_.find(requestedTileKey) != desiredTileKeys_.end()) { + return requestedTileKey; + } + + // Single-stage datasources legitimately return stage-less tiles even when + // the client used staged bucket requests. Treat stage 0 and "unspecified" + // as equivalent only for those layers. + if (advertisedStages <= 1U) { + if (requestedTileKey.stage_ == UnspecifiedStage) { + requestedTileKey.stage_ = 0; + if (desiredTileKeys_.find(requestedTileKey) != desiredTileKeys_.end()) { + return requestedTileKey; + } + } else if (requestedTileKey.stage_ == 0) { + requestedTileKey.stage_ = UnspecifiedStage; + if (desiredTileKeys_.find(requestedTileKey) != desiredTileKeys_.end()) { + return requestedTileKey; + } + } + } + + return std::nullopt; + } + /// Complete all currently pending pull waiters with one terminal status. void collectAllPullWaitersLocked(PullFrameResult::Status status, std::vector& dispatches) { @@ -1065,8 +1095,6 @@ class TilesWsSession : public std::enable_shared_from_this return; if (!layer) return; - - const auto requestedTileKey = makeCanonicalRequestedTileKey(layer->id()); std::optional> stringPoolCommit; std::vector pullDispatches; @@ -1074,8 +1102,11 @@ class TilesWsSession : public std::enable_shared_from_this std::lock_guard lock(mutex_); if (cancelled_) return; + auto requestedTileKey = matchDesiredTileKeyLocked( + layer->id(), + layer->layerInfo() ? std::max(1U, layer->layerInfo()->stages_) : 1U); // Late-arriving tile for an outdated request: drop before serialization work. - if (desiredTileKeys_.find(requestedTileKey) == desiredTileKeys_.end()) { + if (!requestedTileKey.has_value()) { return; } @@ -1107,11 +1138,11 @@ class TilesWsSession : public std::enable_shared_from_this frame.type = m.type; if (m.type == TileLayerStream::MessageType::StringPool) { frame.stringPoolCommit = stringPoolCommit; - frame.requestedTileKey = requestedTileKey; + frame.requestedTileKey = *requestedTileKey; } if (m.type == TileLayerStream::MessageType::TileFeatureLayer || m.type == TileLayerStream::MessageType::TileSourceDataLayer) { - frame.requestedTileKey = requestedTileKey; + frame.requestedTileKey = *requestedTileKey; } enqueueOutgoingLocked(std::move(frame)); } diff --git a/test/unit/test-http-datasource.cpp b/test/unit/test-http-datasource.cpp index 3e3d8d1d..521f2c1c 100644 --- a/test/unit/test-http-datasource.cpp +++ b/test/unit/test-http-datasource.cpp @@ -746,6 +746,25 @@ TEST_CASE("HttpDataSource", "[HttpDataSource]") wsClient.stop(); } + + // WebSocket tiles: single-stage layers must also work through staged bucket requests. + { + auto req = nlohmann::json::object({ + {"requests", nlohmann::json::array({nlohmann::json::object({ + {"mapId", "Tropico"}, + {"layerId", "WayLayer"}, + {"tileIdsByNextStage", nlohmann::json::array({ + nlohmann::json::array({1234}), + })}, + })})}, + }).dump(); + + auto [status, wsTileCount] = runWsTilesRequest(true, req); + REQUIRE(wsTileCount == 1); + REQUIRE(status["requests"].size() == 1); + REQUIRE(status["requests"][0]["status"].get() == + static_cast(RequestStatus::Success)); + } } service.remove(remoteDataSource); From ef82f6082685d15d15c309f78f6efd1d8c1f6d01 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Mon, 30 Mar 2026 17:55:22 +0200 Subject: [PATCH 2/2] Fix search crash caused by function signature mismatch --- libs/model/src/featureid.cpp | 18 ++++++++++++------ libs/model/src/featurelayer.cpp | 24 +++++++++++++++--------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/libs/model/src/featureid.cpp b/libs/model/src/featureid.cpp index fd3d8320..ce6b3598 100644 --- a/libs/model/src/featureid.cpp +++ b/libs/model/src/featureid.cpp @@ -2,7 +2,9 @@ #include "featurelayer.h" #include -#include +#include + +#include #include "mapget/log.h" @@ -143,7 +145,7 @@ void appendTypedKeyValue( valueNode->value()); } -void appendNodeValueToString(std::stringstream& out, simfil::ModelNode::Ptr const& node) +void appendNodeValueToString(std::string& out, simfil::ModelNode::Ptr const& node) { if (!node) { return; @@ -157,7 +159,12 @@ void appendNodeValueToString(std::stringstream& out, simfil::ModelNode::Ptr cons raiseFmt("FeatureId part value 'b\"{}\"' cannot be a ByteArray.", v.toHex()); } else if constexpr (!std::is_same_v) { - out << "." << v; + if constexpr (std::is_same_v) { + fmt::format_to(std::back_inserter(out), FMT_STRING(".{:d}"), v); + } + else { + fmt::format_to(std::back_inserter(out), FMT_STRING(".{}"), v); + } } }, node->value()); @@ -208,8 +215,7 @@ std::string_view FeatureId::typeId() const std::string FeatureId::toString() const { - std::stringstream result; - result << typeId(); + std::string result(typeId()); if (data_.useCommonTilePrefix_) { if (auto idPrefix = model().getIdPrefix()) { @@ -225,7 +231,7 @@ std::string FeatureId::toString() const } } - return result.str(); + return result; } simfil::ValueType FeatureId::type() const diff --git a/libs/model/src/featurelayer.cpp b/libs/model/src/featurelayer.cpp index e05c6a58..0ef9d5cf 100644 --- a/libs/model/src/featurelayer.cpp +++ b/libs/model/src/featurelayer.cpp @@ -501,18 +501,24 @@ namespace * Create a string representation of the given id parts. */ std::string idPartsToString(KeyValueViewPairs const& idParts) { - std::stringstream result; - result << "{"; + fmt::memory_buffer result; + fmt::format_to(std::back_inserter(result), FMT_STRING("{{")); for (auto i = 0; i < idParts.size(); ++i) { - if (i > 0) - result << ", "; - result << idParts[i].first << ": "; - std::visit([&result](auto&& value){ - result << value; + if (i > 0) { + fmt::format_to(std::back_inserter(result), FMT_STRING(", ")); + } + std::visit([&result, key = idParts[i].first](auto&& value){ + using T = std::decay_t; + if constexpr (std::is_same_v) { + fmt::format_to(std::back_inserter(result), FMT_STRING("{}: {:d}"), key, value); + } + else { + fmt::format_to(std::back_inserter(result), FMT_STRING("{}: {}"), key, value); + } }, idParts[i].second); } - result << "}"; - return result.str(); + fmt::format_to(std::back_inserter(result), FMT_STRING("}}")); + return fmt::to_string(result); } /**