retryTransientSessionFileIo<T> function
Runs operation — one FileSystem-shaped call returning a
Result — retrying while it fails with a not-found-shaped
FileError (the transient-ENOENT shape; every other error code is
returned immediately). The last Err is returned as-is on
exhaustion, so the caller's _fsOrThrow names it
(SessionErrorCode.notFound via the existing mapping). Success and
non-retryable failures are indistinguishable from an un-wrapped call.
op names the operation in the log line (open / create /
append); path is the addressed session file.
Implementation
Future<Result<T, FileError>> retryTransientSessionFileIo<T>(
Future<Result<T, FileError>> Function() operation, {
required String op,
required String path,
SessionIoRetryConfig config = const SessionIoRetryConfig(),
}) async {
final policy = config.policy;
var attempt = 0;
var waited = Duration.zero;
var backoff = policy.initialBackoff;
while (true) {
final result = await operation();
attempt++;
if (!result.isErr) return result;
final error = result.errorOrNull!;
if (error.code != FileErrorCode.notFound) return result;
if (attempt >= policy.maxAttempts) return result;
final remaining = policy.maxTotalBackoff - waited;
if (remaining <= Duration.zero) return result;
if (backoff > remaining) backoff = remaining;
waited += backoff;
final retryIn = backoff;
backoff *= policy.multiplier;
// One line per retry: the next attempt's number, the wait, the file,
// and the underlying error — post-mortem greps start at
// `session_io_retry`.
config.logger?.call(
'session_io_retry op=$op attempt=${attempt + 1}/${policy.maxAttempts} '
'path=$path waitMs=${retryIn.inMilliseconds} error=${error.message}',
);
await config.delay(retryIn);
}
}