refreshSession method

Future<void> refreshSession({
  1. UserId? userId,
})

Refreshes Credentials of the account with the provided userId or of the active one, if userId is not provided.

Implementation

Future<void> refreshSession({UserId? userId}) async {
  final int attempt = _refreshAttempt++;

  final FutureOr<bool> futureOrBool = WebUtils.isLocked;
  final bool isLocked = futureOrBool is bool
      ? futureOrBool
      : await futureOrBool;

  userId ??= this.userId;
  final bool areCurrent = userId == this.userId;

  Log.debug(
    'refreshSession($userId |-> $attempt) with `isLocked`: $isLocked',
    '$runtimeType',
  );

  LockIdentifier? dbLock;

  try {
    // Acquire a database lock to prevent multiple refreshes of the same
    // [Credentials] from multiple processes.
    dbLock = await _lockProvider.acquire('refreshSession($userId)');

    // Wait for the lock to be released and check the [Credentials] again as
    // some other task may have already refreshed them.
    await WebUtils.protect(() async {
      Log.debug(
        'refreshSession($userId |-> $attempt) acquired both `dbLock` and `WebUtils.protect()`',
        '$runtimeType',
      );

      Credentials? oldCreds;

      if (userId != null) {
        oldCreds = await _credentialsProvider.read(userId, refresh: true);

        Log.debug(
          'refreshSession($userId |-> $attempt) read from `drift` the `oldCreds`: $oldCreds',
          '$runtimeType',
        );
      }

      if (areCurrent) {
        Log.debug(
          'refreshSession($userId |-> $attempt) `areCurrent` is `true`, which will apply `credentials.value` to `oldCreds` if ${oldCreds == null} -> ${credentials.value}',
          '$runtimeType',
        );

        oldCreds ??= credentials.value;
      }

      if (userId == null) {
        Log.warning(
          'refreshSession($userId |-> $attempt): `userId` is `null`, unable to proceed',
          '$runtimeType',
        );

        return _refreshRetryDelay = _initialRetryDelay;
      }

      if (oldCreds != null) {
        accounts[userId]?.value = oldCreds;
      } else {
        accounts.remove(userId);
      }

      // Ensure the retrieved credentials are the current ones, or otherwise
      // authorize with those.
      if (oldCreds != null &&
          oldCreds.access.secret != credentials.value?.access.secret &&
          !_shouldRefresh(oldCreds)) {
        Log.debug(
          'refreshSession($userId |-> $attempt): false alarm, applying the retrieved fresh credentials',
          '$runtimeType',
        );

        if (areCurrent) {
          await _authorized(oldCreds);
          status.value = RxStatus.success();
        } else {
          // [Credentials] of another account were refreshed.
          _putCredentials(oldCreds);
        }

        return _refreshRetryDelay = _initialRetryDelay;
      }

      if (isLocked) {
        Log.debug(
          'refreshSession($userId |-> $attempt): acquired the lock, while it was locked -> should refresh: ${_shouldRefresh(oldCreds)}',
          '$runtimeType',
        );
      } else {
        Log.debug(
          'refreshSession($userId |-> $attempt): acquired the lock, while it was unlocked -> should refresh: ${_shouldRefresh(oldCreds)}',
          '$runtimeType',
        );
      }

      if (!_shouldRefresh(oldCreds)) {
        if (oldCreds != null) {
          if (credentials.value?.access.secret != oldCreds.access.secret ||
              credentials.value?.refresh.secret != oldCreds.refresh.secret) {
            Log.debug(
              'refreshSession($userId |-> $attempt): `credentials.value` differ from `oldCreds`, thus (since `_shouldRefresh` is `false`) authorizing those',
              '$runtimeType',
            );

            _authorized(oldCreds);
          }
        }

        // [Credentials] are fresh.
        return _refreshRetryDelay = _initialRetryDelay;
      }

      if (oldCreds == null) {
        Log.debug(
          'refreshSession($userId |-> $attempt): `oldCreds` are `null`, seems like during the lock those were removed -> unauthorized',
          '$runtimeType',
        );

        // These [Credentials] were removed while we've been waiting for the
        // lock to be released.
        if (areCurrent) {
          router.go(_unauthorized());
        }

        return _refreshRetryDelay = _initialRetryDelay;
      }

      if (!PlatformUtils.isDeltaSynchronized.value) {
        if (WebUtils.containsCalls() || hasCalls?.call() == true) {
          Log.debug(
            'refreshSession($userId |-> $attempt) should wait for application to be active, however there are calls active, thus ignoring the check',
            '$runtimeType',
          );
        } else {
          Log.debug(
            'refreshSession($userId |-> $attempt) waiting for application to be active...',
            '$runtimeType',
          );

          Completer? completer = Completer();

          // Check for calls in period to proceed refreshing the session if
          // any.
          while (completer?.isCompleted != false) {
            _deltaMutex
                .acquire()
                .then((_) => completer?.complete())
                .catchError((_) => completer?.complete());

            await Future.delayed(Duration(seconds: 2));

            if (WebUtils.containsCalls() || hasCalls?.call() == true) {
              Log.debug(
                'refreshSession($userId |-> $attempt) waiting for application to be active... seems like there are calls active, thus ignoring the check',
                '$runtimeType',
              );

              try {
                completer?.complete();
              } catch (_) {
                completer = null;
                // No-op.
              }
            }
          }

          Log.debug(
            'refreshSession($userId |-> $attempt) waiting for application to be active... done! ✨',
            '$runtimeType',
          );

          if (_deltaMutex.isLocked) {
            _deltaMutex.release();
          }
        }
      }

      try {
        final Credentials data = await _authRepository.refreshSession(
          oldCreds.refresh.secret,
          reconnect: areCurrent,
        );

        Log.debug(
          'refreshSession($userId |-> $attempt): success 🎉 -> writing to `drift`... ✍️',
          '$runtimeType',
        );

        if (areCurrent) {
          await _authorized(data);
        } else {
          // [Credentials] of not currently active account were updated,
          // just save them.
          //
          // Saving to local storage is safe here, as this callback is
          // guarded by the [WebUtils.protect] lock.
          await _credentialsProvider.upsert(data);
          _putCredentials(data);
        }

        Log.debug(
          'refreshSession($userId |-> $attempt): success 🎉 -> writing to `drift`... done ✅',
          '$runtimeType',
        );

        _refreshRetryDelay = _initialRetryDelay;
        status.value = RxStatus.success();
      } on RefreshSessionException catch (_) {
        Log.debug(
          'refreshSession($userId |-> $attempt): ⛔️ `RefreshSessionException` occurred ⛔️, removing credentials',
          '$runtimeType',
        );

        if (areCurrent) {
          router.go(_unauthorized());
        } else {
          // Remove stale [Credentials].
          accounts.remove(oldCreds.userId);
          await _credentialsProvider.delete(oldCreds.userId);
        }

        _refreshRetryDelay = _initialRetryDelay;
        rethrow;
      }
    });

    await _lockProvider.release(dbLock);
  } on RefreshSessionException catch (_) {
    _refreshRetryDelay = _initialRetryDelay;

    if (dbLock != null) {
      await _lockProvider.release(dbLock);
    }

    rethrow;
  } catch (e) {
    Log.debug(
      'refreshSession($userId |-> $attempt): ⛔️ exception occurred: $e',
      '$runtimeType',
    );

    if (dbLock != null) {
      await _lockProvider.release(dbLock);
    }

    // If any unexpected exception happens, just retry the mutation.
    await Future.delayed(_refreshRetryDelay);
    if (_refreshRetryDelay.inSeconds < 12) {
      _refreshRetryDelay = _refreshRetryDelay * 2;
    }

    Log.debug(
      'refreshSession($userId |-> $attempt): backoff passed, trying again',
      '$runtimeType',
    );

    await refreshSession(userId: userId);
  }
}