v0.3.0 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
28inline std::size_t hash_combine(std::size_t seed, std::size_t v) {
29 return seed ^ (v + 0x9e3779b97f4a7c15ull + (seed << 6) + (seed >> 2));
30}
31
32inline 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.
44template<class State, class... CtorArgs>
45State& 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
59template<class Clock>
60bool 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
69inline 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
84template<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
108template<class Fn>
109decltype(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
141template<class ThenFn, class ElseFn>
142decltype(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
194template<class Fn>
195decltype(auto) on(long long invocation, Fn&& fn, std::source_location loc = std::source_location::current()) {
196 using R = std::invoke_result_t<Fn&>;
197
198 struct State {
199 long long count = 0;
200 long long target;
201 bool done = false;
202
203 explicit State(long long t):
204 target(t < 1 ? 1 : t) {}
205 };
206
207 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocation);
208
209 if constexpr (std::is_void_v<R>) {
210 if (!s.done && (++s.count == s.target)) {
211 s.done = true;
212 std::invoke(std::forward<Fn>(fn));
213 }
214 return;
215 }
216 else {
217 using T = std::decay_t<R>;
218 if (!s.done && (++s.count == s.target)) {
219 s.done = true;
220 return std::optional<T> {std::invoke(std::forward<Fn>(fn))};
221 }
222 return std::optional<T> {};
223 }
224}
225
231template<class ThenFn, class ElseFn>
232decltype(auto) on_else(long long invocation,
233 ThenFn&& then_fn,
234 ElseFn&& else_fn,
235 std::source_location loc = std::source_location::current()) {
236 using R1 = std::invoke_result_t<ThenFn&>;
237 using R2 = std::invoke_result_t<ElseFn&>;
238
239 static_assert(std::is_void_v<R1> == std::is_void_v<R2>,
240 "on_else: then/else must both return void or both return a value");
241 if constexpr (!std::is_void_v<R1>) {
242 static_assert(std::is_same_v<std::decay_t<R1>, std::decay_t<R2>>,
243 "on_else: then/else must return the same type");
244 }
245
246 struct State {
247 long long count = 0;
248 long long target;
249 bool done = false;
250
251 explicit State(long long t):
252 target(t < 1 ? 1 : t) {}
253 };
254
255 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocation);
256
257 const bool fire = (!s.done && (++s.count == s.target));
258 if (fire) {
259 s.done = true;
260 }
261
262 if constexpr (std::is_void_v<R1>) {
263 if (fire) {
264 std::invoke(std::forward<ThenFn>(then_fn));
265 }
266 else {
267 std::invoke(std::forward<ElseFn>(else_fn));
268 }
269 return;
270 }
271 else {
272 return fire ? std::invoke(std::forward<ThenFn>(then_fn)) : std::invoke(std::forward<ElseFn>(else_fn));
273 }
274}
275
282template<class Fn>
283decltype(auto) after(long long invocations,
284 Fn&& fn,
285 std::source_location loc = std::source_location::current()) {
286 using R = std::invoke_result_t<Fn&>;
287
288 struct State {
289 long long count = 0;
290 long long target;
291
292 explicit State(long long t):
293 target(t < 0 ? 0 : t) {}
294 };
295
296 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocations);
297
298 const bool fire = (s.count++ >= s.target);
299
300 if constexpr (std::is_void_v<R>) {
301 if (fire) {
302 std::invoke(std::forward<Fn>(fn));
303 }
304 return;
305 }
306 else {
307 using T = std::decay_t<R>;
308 if (fire) {
309 return std::optional<T> {std::invoke(std::forward<Fn>(fn))};
310 }
311 return std::optional<T> {};
312 }
313}
314
320template<class ThenFn, class ElseFn>
321decltype(auto) after_else(long long invocations,
322 ThenFn&& then_fn,
323 ElseFn&& else_fn,
324 std::source_location loc = std::source_location::current()) {
325 using R1 = std::invoke_result_t<ThenFn&>;
326 using R2 = std::invoke_result_t<ElseFn&>;
327
328 static_assert(std::is_void_v<R1> == std::is_void_v<R2>,
329 "after_else: then/else must both return void or both return a value");
330 if constexpr (!std::is_void_v<R1>) {
331 static_assert(std::is_same_v<std::decay_t<R1>, std::decay_t<R2>>,
332 "after_else: then/else must return the same type");
333 }
334
335 struct State {
336 long long count = 0;
337 long long target;
338
339 explicit State(long long t):
340 target(t < 0 ? 0 : t) {}
341 };
342
343 auto& s = ::a3d::util::flow::detail::state_for<State>(loc, invocations);
344
345 const bool fire = (s.count++ >= s.target);
346
347 if constexpr (std::is_void_v<R1>) {
348 if (fire) {
349 std::invoke(std::forward<ThenFn>(then_fn));
350 }
351 else {
352 std::invoke(std::forward<ElseFn>(else_fn));
353 }
354 return;
355 }
356 else {
357 return fire ? std::invoke(std::forward<ThenFn>(then_fn)) : std::invoke(std::forward<ElseFn>(else_fn));
358 }
359}
360
361} // namespace a3d::util::flow
362
363#endif // AVARA3D_UTIL_FLOW_H