@@ -963,6 +963,132 @@ class Notecard {
963963 return p;
964964 }
965965
966+ // ── Operation-session guards ──────────────────────────────────────────
967+ //
968+ // Two independent RAII guards an app holds across a GROUP of requests.
969+ //
970+ // exclusive() — holds the recursive request lock for the guard's lifetime
971+ // so a group of requests is atomic against other threads (exclusion only).
972+ // Inner run_operation() calls re-acquire the lock harmlessly (recursive).
973+ // No-op when no request lock is configured.
974+ //
975+ // keep_ready() — holds the RTX/CTX readiness scope (op_depth_ /
976+ // begin_operation / end_operation) for the guard's lifetime so the card
977+ // is asserted-ready once for the whole group instead of per request
978+ // (readiness only). Inner run_operation() calls see op_depth_ > 0 and
979+ // skip begin_operation. When NOTE_TXN_HANDSHAKE is off, returns a trivial
980+ // empty guard with no op_depth_ touch. No-op when no transport is set.
981+ //
982+ // Both guards: non-copyable, non-movable. C++17 guaranteed copy elision
983+ // (NRVO) makes `auto g = nc.exclusive();` work without any copy or move.
984+ //
985+ // The two guards are INDEPENDENT — an app may use either, both, or neither.
986+ // exclusive() = exclusion; keep_ready() = readiness. They compose correctly:
987+ //
988+ // auto ex = nc.exclusive(); // lock the group
989+ // auto kr = nc.keep_ready(); // hold RTX-ready for the group
990+ // nc.execute(req1);
991+ // nc.execute(req2);
992+
993+ // / RAII guard returned by exclusive(). Holds the recursive request lock
994+ // / for its lifetime; non-copyable, non-movable.
995+ struct ExclusiveSession {
996+ ExclusiveSession (const ExclusiveSession&) = delete ;
997+ ExclusiveSession& operator =(const ExclusiveSession&) = delete ;
998+ ExclusiveSession (ExclusiveSession&&) = delete ;
999+ ExclusiveSession& operator =(ExclusiveSession&&) = delete ;
1000+
1001+ ~ExclusiveSession () {
1002+ if (lock_) lock_->unlock ();
1003+ }
1004+ private:
1005+ friend class Notecard ;
1006+ explicit ExclusiveSession (IBusLock* lock) : lock_(lock) {
1007+ if (lock_) lock_->lock ();
1008+ }
1009+ IBusLock* lock_;
1010+ };
1011+
1012+ // / RAII guard returned by keep_ready(). Holds the RTX/CTX readiness scope
1013+ // / (op_depth_ / begin_operation / end_operation) for its lifetime so the
1014+ // / card is asserted-ready once for the whole group; non-copyable, non-movable.
1015+ // / When NOTE_TXN_HANDSHAKE is off this is a trivial empty struct.
1016+ struct ReadySession {
1017+ ReadySession (const ReadySession&) = delete ;
1018+ ReadySession& operator =(const ReadySession&) = delete ;
1019+ ReadySession (ReadySession&&) = delete ;
1020+ ReadySession& operator =(ReadySession&&) = delete ;
1021+
1022+ #if NOTE_TXN_HANDSHAKE
1023+ ~ReadySession () {
1024+ if (outermost_ && nc_->transport_ ) nc_->transport_ ->end_operation ();
1025+ --nc_->op_depth_ ;
1026+ }
1027+ #else
1028+ ~ReadySession () = default ;
1029+ #endif
1030+
1031+ private:
1032+ friend class Notecard ;
1033+ #if NOTE_TXN_HANDSHAKE
1034+ explicit ReadySession (Notecard* nc)
1035+ : nc_(nc)
1036+ , outermost_(nc_->op_depth_++ == 0 )
1037+ {
1038+ if (outermost_ && nc_->transport_ )
1039+ nc_->transport_ ->begin_operation (nc_->default_timeout_ms_ );
1040+ }
1041+ Notecard* nc_;
1042+ bool outermost_;
1043+ #else
1044+ explicit ReadySession (Notecard*) {}
1045+ #endif
1046+ };
1047+
1048+ // / Hold the recursive request lock across a group of requests, making the
1049+ // / group atomic against other threads (exclusion only — does NOT affect
1050+ // / the RTX/CTX readiness scope).
1051+ // /
1052+ // / The request lock must be set via set_request_lock() and must be
1053+ // / recursive (see set_request_lock docs). When no lock is configured this
1054+ // / is a no-op. Inner run_operation() calls within the guard re-acquire the
1055+ // / lock on the same thread harmlessly (recursive re-entry); other threads
1056+ // / block on the lock for the lifetime of the guard.
1057+ // /
1058+ // / auto ex = nc.exclusive(); // lock held here
1059+ // / nc.execute(req1); // inner lock re-acquire: no-op (recursive)
1060+ // / nc.execute(req2); // still inside the exclusive group
1061+ // / // ex destructs → lock released
1062+ // /
1063+ // / Note: exclusive() = exclusion only. To hold RTX readiness across the
1064+ // / group as well, combine with keep_ready() (the two are independent).
1065+ [[nodiscard]] ExclusiveSession exclusive () {
1066+ return ExclusiveSession{request_lock_};
1067+ }
1068+
1069+ // / Hold the RTX/CTX readiness scope across a group of requests so the
1070+ // / card is asserted-ready once for the whole group instead of per request
1071+ // / (readiness only — does NOT acquire the request lock).
1072+ // /
1073+ // / When NOTE_TXN_HANDSHAKE is enabled, the guard calls begin_operation()
1074+ // / on the outermost entry and end_operation() on destruction. Inner
1075+ // / run_operation() calls see op_depth_ > 0 and skip begin_operation —
1076+ // / the card stays asserted-ready for all requests in the group.
1077+ // /
1078+ // / When NOTE_TXN_HANDSHAKE is off (the default for AVR/minimal builds)
1079+ // / this returns a trivial empty guard with no op_depth_ touch.
1080+ // /
1081+ // / auto kr = nc.keep_ready(); // begin_operation once (RTX asserted)
1082+ // / nc.execute(req1); // op_depth_ > 0 → no inner begin_operation
1083+ // / nc.execute(req2); // same
1084+ // / // kr destructs → end_operation (RTX released)
1085+ // /
1086+ // / Note: keep_ready() = readiness only. To atomically exclude other threads
1087+ // / as well, combine with exclusive() (the two are independent).
1088+ [[nodiscard]] ReadySession keep_ready () {
1089+ return ReadySession{this };
1090+ }
1091+
9661092 // / Operation gate. Acquires the operation lock (if any) before doing
9671093 // / anything else, so that cross-thread exclusion is immediate: a second
9681094 // / thread entering any operation blocks here until the first thread's
0 commit comments