@@ -319,45 +319,38 @@ class Notecard {
319319 ApiResult<typename RequestT::Response> execute (const RequestT& req) {
320320 return run_operation ([&]() -> ApiResult<typename RequestT::Response> {
321321 using Rsp = typename RequestT::Response;
322-
323- debug_timing (debug_, TimingEvent::TransactionBegin, RequestT::notecard_request);
324-
325- if (!transport_) {
326- debug_timing (debug_, TimingEvent::TransactionEnd, RequestT::notecard_request);
327- return Unexpected (make_error (Error::NotReady, NOTE_ERR (" no transport configured" )));
328- }
329-
330- // Streaming path: SAX-stream the response straight into the
331- // typed Rsp::Sink, no intermediate tree. Parallel to
332- // execute_tree below — the two are siblings, picked at this
333- // call site by (a) whether the response type has a sink and
334- // (b) whether an allocator is configured.
335- if constexpr (std::is_void_v<Rsp> || detail::has_sink<Rsp>::value) {
336- if (alloc_.has_value ()) {
337- auto result = execute_streaming (req);
338- debug_timing (debug_, TimingEvent::TransactionEnd, RequestT::notecard_request);
339- return result;
322+ constexpr bool can_stream =
323+ std::is_void_v<Rsp> || detail::has_sink<Rsp>::value;
324+
325+ // Type-independent prologue (timing + path selection) — see
326+ // begin_execute(). Only the type-dependent calls below stay here.
327+ ExecDecision d = begin_execute (can_stream, RequestT::notecard_request);
328+
329+ ApiResult<Rsp> result = [&]() -> ApiResult<Rsp> {
330+ // Streaming path: SAX-stream the response straight into the
331+ // typed Rsp::Sink, no intermediate tree.
332+ if constexpr (can_stream) {
333+ if (d.path == ExecPath::Streaming) {
334+ return execute_streaming (req);
335+ }
340336 }
341- }
342-
343- // Buffered fallback: requires a JsonBackend + buffered transport.
337+ // Buffered fallback: JsonBackend + buffered transport, retried.
344338#if !NOTE_NO_JSON_TREE
345- if (backend_) {
346- const uint32_t req_id = request_ids_enabled_ ? next_request_id_++ : 0 ;
347- auto attempt = [&]() -> ApiResult<Rsp> {
348- return execute_tree (req, req_id);
349- };
350- auto reset = [&]() { transport_->reset (); };
351-
352- auto result = retry_transaction<ApiResult<Rsp>>(
353- hal (), timing_, RequestT::safety, retry_policy_,
354- attempt, reset);
355- debug_timing (debug_, TimingEvent::TransactionEnd, RequestT::notecard_request);
356- return result;
357- }
339+ if (d.path == ExecPath::Buffered) {
340+ auto attempt = [&]() -> ApiResult<Rsp> {
341+ return execute_tree (req, d.req_id );
342+ };
343+ auto reset = [&]() { transport_->reset (); };
344+ return retry_transaction<ApiResult<Rsp>>(
345+ hal (), timing_, RequestT::safety, retry_policy_,
346+ attempt, reset);
347+ }
358348#endif
359- debug_timing (debug_, TimingEvent::TransactionEnd, RequestT::notecard_request);
360- return Unexpected (make_error (Error::NotReady, NOTE_ERR (" no backend or streaming transport configured" )));
349+ return *d.error ;
350+ }();
351+
352+ end_execute (RequestT::notecard_request);
353+ return result;
361354 });
362355 }
363356
@@ -940,6 +933,50 @@ class Notecard {
940933 return request_ids_enabled_ ? next_request_id_++ : 0 ;
941934 }
942935
936+ // ── Type-independent execute() core ─────────────────────────────────────
937+ // execute<RequestT>() is instantiated once per request type, so any branch
938+ // in its body is counted (and code-generated) once per type. The config
939+ // checks below (transport/streaming/buffered selection, the debug-timing
940+ // null checks) are identical across every RequestT, so they live here in
941+ // non-template members: emitted and measured once, not per instantiation.
942+ // Only the genuinely type-dependent calls (execute_streaming/execute_tree)
943+ // stay in the template. See DESIGN-branch-coverage-floor.md.
944+ enum class ExecPath : uint8_t { Streaming, Buffered, Error };
945+ struct ExecDecision {
946+ ExecPath path = ExecPath::Error;
947+ uint32_t req_id = 0 ;
948+ std::optional<Unexpected> error; // set iff path == Error
949+ };
950+
951+ // Prologue: emit begin-timing, validate config, choose the dispatch path.
952+ // `can_stream` is the compile-time streamability of the response type.
953+ ExecDecision begin_execute (bool can_stream, string_view req_name) {
954+ debug_timing (debug_, TimingEvent::TransactionBegin, req_name);
955+ ExecDecision d;
956+ if (!transport_) {
957+ d.error = make_error (Error::NotReady, NOTE_ERR (" no transport configured" ));
958+ return d;
959+ }
960+ if (can_stream && alloc_.has_value ()) {
961+ d.path = ExecPath::Streaming;
962+ return d;
963+ }
964+ #if !NOTE_NO_JSON_TREE
965+ if (backend_) {
966+ d.path = ExecPath::Buffered;
967+ d.req_id = next_request_id_or_zero ();
968+ return d;
969+ }
970+ #endif
971+ d.error = make_error (Error::NotReady, NOTE_ERR (" no backend or streaming transport configured" ));
972+ return d;
973+ }
974+
975+ // Epilogue: emit end-timing. Called once per execute(), regardless of path.
976+ void end_execute (string_view req_name) {
977+ debug_timing (debug_, TimingEvent::TransactionEnd, req_name);
978+ }
979+
943980 // / Allocator-backed durable copy of a Notecard error message — used
944981 // / by the Api singleton thunk path to give `NcErrorCapture::view()`
945982 // / a pointer that outlives the caller-stack-allocated NcErrorCapture
0 commit comments