refreshSession method
- 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);
}
}