runStartedSpend<T> function

Future<StartedSpend<T>> runStartedSpend<T>({
  1. required Future<T> authorize(
    1. SpendStart<T> start
    ),
  2. required bool precedesPersistence(
    1. Object error
    ),
  3. required String atMostOnceMessage,
  4. void onEntered()?,
})

Run one spend through the host's authorizer and say what became of it.

authorize calls the authorizer; its closure runs its own fences, then returns start(() => session.send(...)). start enforces AT MOST ONCE (a second run throws StateError with atMostOnceMessage — a second proposal token, or on the queue a second payment), runs beforeEnter, marks the spend started (and calls onEntered), and records the SDK call's result or its own throw as they happen.

Never throws: every error comes back inside the verdict.

Implementation

Future<StartedSpend<T>> runStartedSpend<T>({
  required Future<T> Function(SpendStart<T> start) authorize,
  required bool Function(Object error) precedesPersistence,
  required String atMostOnceMessage,
  void Function()? onEntered,
}) async {
  var entered = false;
  var hasLanded = false;
  T? landed;
  Object? sdkThrew;

  Future<T> start(
    Future<T> Function() sdkCall, {
    void Function()? beforeEnter,
  }) {
    if (entered) throw StateError(atMostOnceMessage);
    beforeEnter?.call();
    // Past this line a transaction may exist no matter what happens next.
    entered = true;
    onEntered?.call();
    return sdkCall().then(
      (value) {
        landed = value;
        hasLanded = true;
        return value;
      },
      onError: (Object error, StackTrace stack) {
        sdkThrew = error;
        Error.throwWithStackTrace(error, stack);
      },
    );
  }

  try {
    await authorize(start);
  } catch (error) {
    if (hasLanded) return SpendLanded<T>(landed as T, errorAfter: error);
    if (!entered) return SpendNotStarted<T>(error);
    if (identical(error, sdkThrew) && precedesPersistence(error)) {
      return SpendRefusedBeforePersist<T>(error);
    }
    return SpendAnswerLost<T>(error);
  }
  if (hasLanded) return SpendLanded<T>(landed as T);
  if (entered) return SpendAnswerLost<T>(null);
  return SpendNotStarted<T>(null);
}