Avara3D 0.2.0
C++ API reference
Flow.h
1//
2// Flow.h
3// avara3d
4//
5// Created by Morgan Davis on 1/1/26.
6// Copyright © 2026 Morgan K Davis. All rights reserved.
7//
8
9#ifndef AVARA3D_UTIL_FLOW_H
10#define AVARA3D_UTIL_FLOW_H
11
12#include <chrono>
13#include <cstdint>
14#include <functional>
15#include <memory>
16#include <mutex>
17#include <optional>
18#include <source_location>
19#include <string_view>
20#include <type_traits>
21#include <unordered_map>
22#include <utility>
23
24// TODO: summary
25
26namespace a3d::util::flow::detail {
27
28 inline std::size_t hash_combine(std::size_t seed, std::size_t v) {
29 return seed ^ (v + 0x9e3779b97f4a7c15ull + (seed << 6) + (seed >> 2));
30 }
31
32 inline std::size_t callsite_key(std::source_location loc) {
33 std::size_t h = 0;
34 std::string_view file = loc.file_name();
35 std::string_view func = loc.function_name();
36 h = hash_combine(h, std::hash<std::string_view> {}(file));
37 h = hash_combine(h, std::hash<std::string_view> {}(func));
38 h = hash_combine(h, std::hash<unsigned> {}(loc.line()));
39 h = hash_combine(h, std::hash<unsigned> {}(loc.column()));
40 return h;
41 }
42
43 // Per-(State type, TU) map keyed by callsite.
44 template<class State, class... CtorArgs>
45 State& state_for(std::source_location loc, CtorArgs&&... ctorArgs) {
46 static std::mutex m;
47 static std::unordered_map<std::size_t, std::unique_ptr<State>> states;
48
49 const std::size_t key = callsite_key(loc);
50
51 std::lock_guard<std::mutex> lock(m);
52 auto& ptr = states[key];
53 if (!ptr) {
54 ptr = std::make_unique<State>(std::forward<CtorArgs>(ctorArgs)...);
55 }
56 return *ptr;
57 }
58
59 template<class Clock>
60 bool every_tick(typename Clock::time_point& last, typename Clock::duration interval) {
61 const auto now = Clock::now();
62 if (now - last >= interval) {
63 last = now; // "at most once" behavior (no catch-up)
64 return true;
65 }
66 return false;
67 }
68
69 inline long long clamp_step(long long n) {
70 return (n < 1) ? 1 : n;
71 }
72
73} // namespace a3d::util::flow::detail
74
75namespace a3d::util::flow {
76
77 // [Public Functions]
78
84 template<class Cond, class FailFn>
85 [[nodiscard]] bool edge_guard(Cond&& cond,
86 FailFn&& on_fail_once,
87 std::source_location loc = std::source_location::current()) {
88 struct State {
89 bool latched = false;
90 };
91
92 const bool ok = static_cast<bool>(std::forward<Cond>(cond)); // supports shared_ptr, etc.
93 auto& s = ::a3d::util::flow::detail::state_for<State>(loc);
94
95 if (ok) {
96 s.latched = false;
97 return true;
98 }
99
100 if (!s.latched) {
101 s.latched = true;
102 std::invoke(std::forward<FailFn>(on_fail_once));
103 }
104 return false;
105 }
106
108 template<class Fn>
109 decltype(auto) once(Fn&& fn, std::source_location loc = std::source_location::current()) {
110 using R = std::invoke_result_t<Fn&>;
111
112 if constexpr (std::is_void_v<R>) {
113 struct State {
114 std::once_flag once;
115 };
116
117 auto& s = ::a3d::util::flow::detail::state_for<State>(loc);
118 std::call_once(s.once, [&] {
119 std::invoke(std::forward<Fn>(fn));
120 });
121 return;
122 }
123 else {
124 using T = std::decay_t<R>;
125
126 struct State {
127 std::once_flag once;
128 std::optional<T> value;
129 };
130
131 auto& s = ::a3d::util::flow::detail::state_for<State>(loc);
132
133 std::call_once(s.once, [&] {
134 s.value.emplace(std::invoke(std::forward<Fn>(fn)));
135 });
136 return static_cast<const T&>(*s.value);
137 }
138 }
139
141 template<class ThenFn, class ElseFn>
142 decltype(auto) once_else(ThenFn&& then_fn,
143 ElseFn&& else_fn,
144 std::source_location loc = std::source_location::current()) {
145 using R1 = std::invoke_result_t<ThenFn&>;
146 using R2 = std::invoke_result_t<ElseFn&>;
147
148 static_assert(std::is_void_v<R1> == std::is_void_v<R2>,
149 "once_else: then/else must both return void or both return a value");
150 if constexpr (!std::is_void_v<R1>) {
151 static_assert(std::is_same_v<std::decay_t<R1>, std::decay_t<R2>>,
152 "once_else: then/else must return the same type");
153 }
154
155 struct State {
156 std::once_flag once;
157 };
158
159 auto& s = ::a3d::util::flow::detail::state_for<State>(loc);
160
161 bool i_ran_then = false;
162 if constexpr (std::is_void_v<R1>) {
163 std::call_once(s.once, [&] {
164 i_ran_then = true;
165 std::invoke(std::forward<ThenFn>(then_fn));
166 });
167 if (!i_ran_then) {
168 std::invoke(std::forward<ElseFn>(else_fn));
169 }
170 return;
171 }
172 else {
173 using T = std::decay_t<R1>;
174 std::optional<T> first_value;
175
176 std::call_once(s.once, [&] {
177 i_ran_then = true;
178 first_value.emplace(std::invoke(std::forward<ThenFn>(then_fn)));
179 });
180
181 if (i_ran_then) {
182 return *first_value; // by value (T)
183 }
184 return std::invoke(std::forward<ElseFn>(else_fn)); // by value (T)
185 }
186 }
187
194 template<class Fn>
195 decltype(auto) on(long long invocation,
196 Fn&& fn,
197 std::source_location loc = std::source_location::current()) {
198 using R = std::invoke_result_t<Fn&>;
199
200 struct State {
201 long long count = 0;
202 long long target;
203 bool done = false;
204
205 explicit State(long long t):
206 target(t < 1 ? 1 : t) {}
207 };
208
209 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocation);
210
211 if constexpr (std::is_void_v<R>) {
212 if (!s.done && (++s.count == s.target)) {
213 s.done = true;
214 std::invoke(std::forward<Fn>(fn));
215 }
216 return;
217 }
218 else {
219 using T = std::decay_t<R>;
220 if (!s.done && (++s.count == s.target)) {
221 s.done = true;
222 return std::optional<T> {std::invoke(std::forward<Fn>(fn))};
223 }
224 return std::optional<T> {};
225 }
226 }
227
233 template<class ThenFn, class ElseFn>
234 decltype(auto) on_else(long long invocation,
235 ThenFn&& then_fn,
236 ElseFn&& else_fn,
237 std::source_location loc = std::source_location::current()) {
238 using R1 = std::invoke_result_t<ThenFn&>;
239 using R2 = std::invoke_result_t<ElseFn&>;
240
241 static_assert(std::is_void_v<R1> == std::is_void_v<R2>,
242 "on_else: then/else must both return void or both return a value");
243 if constexpr (!std::is_void_v<R1>) {
244 static_assert(std::is_same_v<std::decay_t<R1>, std::decay_t<R2>>,
245 "on_else: then/else must return the same type");
246 }
247
248 struct State {
249 long long count = 0;
250 long long target;
251 bool done = false;
252
253 explicit State(long long t):
254 target(t < 1 ? 1 : t) {}
255 };
256
257 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocation);
258
259 const bool fire = (!s.done && (++s.count == s.target));
260 if (fire) {
261 s.done = true;
262 }
263
264 if constexpr (std::is_void_v<R1>) {
265 if (fire) {
266 std::invoke(std::forward<ThenFn>(then_fn));
267 }
268 else {
269 std::invoke(std::forward<ElseFn>(else_fn));
270 }
271 return;
272 }
273 else {
274 return fire ? std::invoke(std::forward<ThenFn>(then_fn))
275 : std::invoke(std::forward<ElseFn>(else_fn));
276 }
277 }
278
285 template<class Fn>
286 decltype(auto) after(long long invocations,
287 Fn&& fn,
288 std::source_location loc = std::source_location::current()) {
289 using R = std::invoke_result_t<Fn&>;
290
291 struct State {
292 long long count = 0;
293 long long target;
294
295 explicit State(long long t):
296 target(t < 0 ? 0 : t) {}
297 };
298
299 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocations);
300
301 const bool fire = (s.count++ >= s.target);
302
303 if constexpr (std::is_void_v<R>) {
304 if (fire) {
305 std::invoke(std::forward<Fn>(fn));
306 }
307 return;
308 }
309 else {
310 using T = std::decay_t<R>;
311 if (fire) {
312 return std::optional<T> {std::invoke(std::forward<Fn>(fn))};
313 }
314 return std::optional<T> {};
315 }
316 }
317
323 template<class ThenFn, class ElseFn>
324 decltype(auto) after_else(long long invocations,
325 ThenFn&& then_fn,
326 ElseFn&& else_fn,
327 std::source_location loc = std::source_location::current()) {
328 using R1 = std::invoke_result_t<ThenFn&>;
329 using R2 = std::invoke_result_t<ElseFn&>;
330
331 static_assert(std::is_void_v<R1> == std::is_void_v<R2>,
332 "after_else: then/else must both return void or both return a value");
333 if constexpr (!std::is_void_v<R1>) {
334 static_assert(std::is_same_v<std::decay_t<R1>, std::decay_t<R2>>,
335 "after_else: then/else must return the same type");
336 }
337
338 struct State {
339 long long count = 0;
340 long long target;
341
342 explicit State(long long t):
343 target(t < 0 ? 0 : t) {}
344 };
345
346 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocations);
347
348 const bool fire = (s.count++ >= s.target);
349
350 if constexpr (std::is_void_v<R1>) {
351 if (fire) {
352 std::invoke(std::forward<ThenFn>(then_fn));
353 }
354 else {
355 std::invoke(std::forward<ElseFn>(else_fn));
356 }
357 return;
358 }
359 else {
360 return fire ? std::invoke(std::forward<ThenFn>(then_fn))
361 : std::invoke(std::forward<ElseFn>(else_fn));
362 }
363 }
364} // namespace a3d::util::flow
365
366#endif // AVARA3D_UTIL_FLOW_H