Trakt was the one service outside the tracker abstraction. TraktScrobbleService re-implemented the whole playback lifecycle beside TrackerCoordinator, and TraktSyncService pushed watched state from its own WatchStateNotifier subscription, so the player called two objects at every lifecycle point and one watch could be written twice. TraktTracker now implements RealtimeScrobbleTracker like Simkl; the duplicated player call sites collapse to one each, and Trakt shares the coordinator's ID resolver instead of re-fetching show ids every episode. Capabilities are split so a tracker declares what it is rather than being special-cased: ScrobblePolicy carries each service's own resend/seek rules, EpisodeHistoryTracker names the remote row a per-item history write targets, and SeriesProgressTracker covers one-counter-per-series services. Writes from all four trackers go through a shared TrackerWriteQueue, generalised from the Trakt-only queue, with the legacy Trakt payload migrated on load. Trakt becomes the fourth TrackersProvider slot and TraktAccountProvider is deleted, so one object owns the active session per profile. Two failure paths found while consolidating are fixed here too. The queue's retries only ran on profile bind, connect and app foreground, so a network blip mid-session left queued watches waiting for the next foreground. OfflineModeProvider now notifies on connectivity changes, not just offline-state or WiFi-flag changes, and main.dart flushes the queue when the network returns. The queue also counted every failure toward the five attempts that permanently drop an item, so a rate limit or a service having a bad hour could discard a pending watch - the loss the queue exists to prevent. Only an answer about the write itself now spends an attempt: 4xx counts, while rate limits, 5xx, recoverable token-refresh failures and requests that never arrived do not. A back-off answer also defers that service for the rest of the flush, so a queue holding many rows does not fire all of them at a service that just asked for quiet.
512 lines
19 KiB
Dart
512 lines
19 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/providers/trackers_provider.dart';
|
|
import 'package:plezy/services/base_shared_preferences_service.dart';
|
|
import 'package:plezy/services/trackers/anilist/anilist_tracker.dart';
|
|
import 'package:plezy/services/trackers/tracker_account_store.dart';
|
|
import 'package:plezy/services/trackers/tracker_constants.dart';
|
|
import 'package:plezy/services/trackers/tracker_coordinator.dart';
|
|
import 'package:plezy/services/trackers/tracker_session.dart';
|
|
import 'package:plezy/services/trackers/mal/mal_tracker.dart';
|
|
import 'package:plezy/services/trackers/simkl/simkl_tracker.dart';
|
|
import 'package:plezy/services/trackers/trakt/trakt_tracker.dart';
|
|
|
|
import '../test_helpers/io_fakes.dart';
|
|
import '../test_helpers/prefs.dart';
|
|
|
|
final _malStore = trackerAccountStore(TrackerService.mal);
|
|
final _anilistStore = trackerAccountStore(TrackerService.anilist);
|
|
final _simklStore = trackerAccountStore(TrackerService.simkl);
|
|
final _traktStore = trackerAccountStore(TrackerService.trakt);
|
|
|
|
TrackerSession _mal({String? username}) => TrackerSession(
|
|
accessToken: 'mal-at',
|
|
refreshToken: 'mal-rt',
|
|
expiresAt: DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600,
|
|
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
username: username,
|
|
);
|
|
|
|
TrackerSession _anilist({String? username}) => TrackerSession(
|
|
accessToken: 'anilist-at',
|
|
expiresAt: DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600,
|
|
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
username: username,
|
|
);
|
|
|
|
TrackerSession _simkl({String? username}) => TrackerSession(
|
|
accessToken: 'simkl-at',
|
|
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
username: username,
|
|
);
|
|
|
|
TrackerSession _trakt({String? username}) => TrackerSession(
|
|
accessToken: 'trakt-at',
|
|
refreshToken: 'trakt-rt',
|
|
expiresAt: DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600,
|
|
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
username: username,
|
|
);
|
|
|
|
Future<void> _bindProfile(TrackersProvider provider, String? userUuid) async {
|
|
await provider.onActiveProfileChanged(userUuid);
|
|
await TrackerCoordinator.instance.flushWriteQueue();
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
resetSharedPreferencesForTest();
|
|
_resetTrackerBindings();
|
|
});
|
|
tearDown(_resetTrackerBindings);
|
|
|
|
group('TrackersProvider', () {
|
|
test('starts with all trackers disconnected', () {
|
|
final p = TrackersProvider();
|
|
expect(p.mal, isNull);
|
|
expect(p.anilist, isNull);
|
|
expect(p.simkl, isNull);
|
|
expect(p.trakt, isNull);
|
|
expect(p.isMalConnected, isFalse);
|
|
expect(p.isAnilistConnected, isFalse);
|
|
expect(p.isSimklConnected, isFalse);
|
|
expect(p.isTraktConnected, isFalse);
|
|
expect(p.malUsername, isNull);
|
|
expect(p.anilistUsername, isNull);
|
|
expect(p.simklUsername, isNull);
|
|
expect(p.traktUsername, isNull);
|
|
expect(p.isConnecting(TrackerService.mal), isFalse);
|
|
expect(p.isConnecting(TrackerService.anilist), isFalse);
|
|
expect(p.isConnecting(TrackerService.simkl), isFalse);
|
|
expect(p.isConnecting(TrackerService.trakt), isFalse);
|
|
p.dispose();
|
|
});
|
|
|
|
test('forTesting owns one fresh client per eager auth owner', () {
|
|
final clients = <FakeHttpClient>[];
|
|
final p = TrackersProvider.forTesting(
|
|
connectPipeline: _ControlledConnectPipeline(_mal()).call,
|
|
httpClientFactory: () {
|
|
final client = FakeHttpClient(200, const <int>[]);
|
|
clients.add(client);
|
|
return client;
|
|
},
|
|
);
|
|
|
|
expect(clients, hasLength(5));
|
|
expect(clients.toSet(), hasLength(5));
|
|
for (final client in clients) {
|
|
expect(client.closeCount, 0);
|
|
}
|
|
|
|
p.dispose();
|
|
|
|
for (final client in clients) {
|
|
expect(client.closeCount, 1);
|
|
expect(client.isClosed, isTrue);
|
|
}
|
|
});
|
|
|
|
test('onActiveProfileChanged loads sessions from per-profile stores', () async {
|
|
const uuid = 'profile-1';
|
|
await _malStore.save(uuid, _mal(username: 'alice'));
|
|
await _anilistStore.save(uuid, _anilist(username: 'bob'));
|
|
await _simklStore.save(uuid, _simkl(username: 'carol'));
|
|
await _traktStore.save(uuid, _trakt(username: 'dave'));
|
|
|
|
// Reset cached singletons so the provider reads fresh prefs state.
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
await _bindProfile(p, uuid);
|
|
expect(p.isMalConnected, isTrue);
|
|
expect(p.isAnilistConnected, isTrue);
|
|
expect(p.isSimklConnected, isTrue);
|
|
expect(p.isTraktConnected, isTrue);
|
|
expect(p.malUsername, 'alice');
|
|
expect(p.anilistUsername, 'bob');
|
|
expect(p.simklUsername, 'carol');
|
|
expect(p.traktUsername, 'dave');
|
|
expect(notified, greaterThanOrEqualTo(1));
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('onActiveProfileChanged switching to empty profile clears all sessions', () async {
|
|
const uuid = 'profile-1';
|
|
await _malStore.save(uuid, _mal(username: 'alice'));
|
|
await _anilistStore.save(uuid, _anilist(username: 'bob'));
|
|
await _simklStore.save(uuid, _simkl(username: 'carol'));
|
|
await _traktStore.save(uuid, _trakt(username: 'dave'));
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
await _bindProfile(p, uuid);
|
|
expect(p.isMalConnected, isTrue);
|
|
|
|
await _bindProfile(p, 'other-profile');
|
|
expect(p.isMalConnected, isFalse);
|
|
expect(p.isAnilistConnected, isFalse);
|
|
expect(p.isSimklConnected, isFalse);
|
|
expect(p.isTraktConnected, isFalse);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('onActiveProfileChanged loads only the populated stores', () async {
|
|
const uuid = 'profile-2';
|
|
// Only AniList is set up — MAL, Simkl, and Trakt remain absent.
|
|
await _anilistStore.save(uuid, _anilist(username: 'bob'));
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
await _bindProfile(p, uuid);
|
|
expect(p.isAnilistConnected, isTrue);
|
|
expect(p.anilistUsername, 'bob');
|
|
expect(p.isMalConnected, isFalse);
|
|
expect(p.isSimklConnected, isFalse);
|
|
expect(p.isTraktConnected, isFalse);
|
|
p.dispose();
|
|
});
|
|
|
|
test('disconnectMal clears stored session and notifies', () async {
|
|
const uuid = 'profile-3';
|
|
await _malStore.save(uuid, _mal(username: 'alice'));
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
await _bindProfile(p, uuid);
|
|
expect(p.isMalConnected, isTrue);
|
|
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
await p.disconnectMal();
|
|
expect(p.isMalConnected, isFalse);
|
|
expect(p.mal, isNull);
|
|
// _clearAndRebind notifies once.
|
|
expect(notified, 1);
|
|
|
|
// Persistence is cleared too.
|
|
expect(await _malStore.load(uuid), isNull);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('disconnectAnilist clears anilist while leaving MAL intact', () async {
|
|
const uuid = 'profile-4';
|
|
await _malStore.save(uuid, _mal(username: 'alice'));
|
|
await _anilistStore.save(uuid, _anilist(username: 'bob'));
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
await _bindProfile(p, uuid);
|
|
|
|
await p.disconnectAnilist();
|
|
expect(p.isAnilistConnected, isFalse);
|
|
expect(p.isMalConnected, isTrue);
|
|
expect(p.malUsername, 'alice');
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('disconnect during an in-flight profile load keeps the other trackers loaded', () async {
|
|
const uuid = 'profile-race';
|
|
await _malStore.save(uuid, _mal(username: 'alice'));
|
|
await _anilistStore.save(uuid, _anilist(username: 'bob'));
|
|
await _simklStore.save(uuid, _simkl(username: 'carol'));
|
|
await _traktStore.save(uuid, _trakt(username: 'dave'));
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final p = TrackersProvider();
|
|
// Start the load, then disconnect MAL before it resolves.
|
|
final load = _bindProfile(p, uuid);
|
|
await p.disconnectMal();
|
|
await load;
|
|
|
|
// MAL stays disconnected (and cleared) — the racing load must not
|
|
// resurrect it — but it also must not drop AniList/Simkl/Trakt.
|
|
expect(p.isMalConnected, isFalse);
|
|
expect(await _malStore.load(uuid), isNull);
|
|
expect(p.isAnilistConnected, isTrue);
|
|
expect(p.anilistUsername, 'bob');
|
|
expect(p.isSimklConnected, isTrue);
|
|
expect(p.simklUsername, 'carol');
|
|
expect(p.isTraktConnected, isTrue);
|
|
expect(p.traktUsername, 'dave');
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('disconnectSimkl on a profile with no session is safe', () async {
|
|
final p = TrackersProvider();
|
|
// No `onActiveProfileChanged` — uuid is empty (global slot).
|
|
// disconnectSimkl just clears the (already absent) entry and rebinds.
|
|
await p.disconnectSimkl();
|
|
expect(p.isSimklConnected, isFalse);
|
|
p.dispose();
|
|
});
|
|
|
|
test('cancelConnect is a no-op when not connecting', () {
|
|
final p = TrackersProvider();
|
|
expect(() => p.cancelConnect(), returnsNormally);
|
|
expect(p.isConnecting(TrackerService.mal), isFalse);
|
|
p.dispose();
|
|
});
|
|
|
|
test('safeNotifyListeners after dispose is a no-op', () async {
|
|
final p = TrackersProvider();
|
|
p.dispose();
|
|
// Post-dispose rebind should not throw.
|
|
await _bindProfile(p, 'any-uuid');
|
|
});
|
|
for (final service in [TrackerService.mal, TrackerService.anilist, TrackerService.simkl, TrackerService.trakt]) {
|
|
test('$service stale connect cannot save or replace a newer binding after dispose', () async {
|
|
const oldUuid = 'profile-old';
|
|
const newUuid = 'profile-new';
|
|
final oldSession = _session(service, 'old');
|
|
final newSession = _session(service, 'new');
|
|
await _store(service).save(newUuid, newSession);
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final pipeline = _ControlledConnectPipeline(oldSession);
|
|
final oldProvider = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(oldProvider, oldUuid);
|
|
final connect = _connect(oldProvider, service);
|
|
await pipeline.beforeSave.future;
|
|
|
|
oldProvider.dispose();
|
|
final newProvider = TrackersProvider();
|
|
await _bindProfile(newProvider, newUuid);
|
|
final newBinding = _boundClient(service);
|
|
expect(newBinding, isNotNull);
|
|
expect(_providerSession(newProvider, service)?.accessToken, newSession.accessToken);
|
|
|
|
pipeline.releaseBeforeSave.complete();
|
|
expect(await connect, isFalse);
|
|
expect(await _store(service).load(oldUuid), isNull);
|
|
expect((await _store(service).load(newUuid))?.accessToken, newSession.accessToken);
|
|
expect(_boundClient(service), same(newBinding));
|
|
expect(_boundSession(service)?.accessToken, newSession.accessToken);
|
|
expect(_providerSession(oldProvider, service), isNull);
|
|
|
|
newProvider.dispose();
|
|
});
|
|
}
|
|
|
|
test('cancel invalidates a connect after authorization and before save', () async {
|
|
const uuid = 'profile-cancel';
|
|
final pipeline = _ControlledConnectPipeline(_session(TrackerService.mal, 'cancelled'));
|
|
final p = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(p, uuid);
|
|
|
|
final connect = _connect(p, TrackerService.mal);
|
|
await pipeline.beforeSave.future;
|
|
p.cancelConnect();
|
|
pipeline.releaseBeforeSave.complete();
|
|
|
|
expect(await connect, isFalse);
|
|
expect(p.isConnecting(TrackerService.mal), isFalse);
|
|
expect(p.mal, isNull);
|
|
expect(await _malStore.load(uuid), isNull);
|
|
expect(MalTracker.instance.client, isNull);
|
|
p.dispose();
|
|
});
|
|
|
|
test('profile change invalidates the old connect and preserves the new binding', () async {
|
|
const oldUuid = 'profile-change-old';
|
|
const newUuid = 'profile-change-new';
|
|
final oldSession = _session(TrackerService.anilist, 'old');
|
|
final newSession = _session(TrackerService.anilist, 'new');
|
|
await _anilistStore.save(newUuid, newSession);
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final pipeline = _ControlledConnectPipeline(oldSession);
|
|
final p = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(p, oldUuid);
|
|
final connect = _connect(p, TrackerService.anilist);
|
|
await pipeline.beforeSave.future;
|
|
|
|
await _bindProfile(p, newUuid);
|
|
final newBinding = AnilistTracker.instance.client;
|
|
pipeline.releaseBeforeSave.complete();
|
|
|
|
expect(await connect, isFalse);
|
|
expect(await _anilistStore.load(oldUuid), isNull);
|
|
expect((await _anilistStore.load(newUuid))?.accessToken, newSession.accessToken);
|
|
expect(p.anilist?.accessToken, newSession.accessToken);
|
|
expect(AnilistTracker.instance.client, same(newBinding));
|
|
expect(AnilistTracker.instance.client?.session.accessToken, newSession.accessToken);
|
|
p.dispose();
|
|
});
|
|
|
|
test('same-service disconnect invalidates an in-flight connect', () async {
|
|
const uuid = 'profile-same-disconnect';
|
|
final pipeline = _ControlledConnectPipeline(_session(TrackerService.simkl, 'late'));
|
|
final p = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(p, uuid);
|
|
final connect = _connect(p, TrackerService.simkl);
|
|
await pipeline.beforeSave.future;
|
|
|
|
await p.disconnectSimkl();
|
|
pipeline.releaseBeforeSave.complete();
|
|
|
|
expect(await connect, isFalse);
|
|
expect(p.simkl, isNull);
|
|
expect(await _simklStore.load(uuid), isNull);
|
|
expect(SimklTracker.instance.client, isNull);
|
|
p.dispose();
|
|
});
|
|
|
|
test('unrelated disconnect leaves an allowed connect current', () async {
|
|
const uuid = 'profile-unrelated-disconnect';
|
|
final linkedAnilist = _session(TrackerService.anilist, 'linked');
|
|
final connectedMal = _session(TrackerService.mal, 'connected');
|
|
await _anilistStore.save(uuid, linkedAnilist);
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
|
|
final pipeline = _ControlledConnectPipeline(connectedMal);
|
|
final p = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(p, uuid);
|
|
final connect = _connect(p, TrackerService.mal);
|
|
await pipeline.beforeSave.future;
|
|
|
|
await p.disconnectAnilist();
|
|
pipeline.releaseBeforeSave.complete();
|
|
|
|
expect(await connect, isTrue);
|
|
await TrackerCoordinator.instance.flushWriteQueue();
|
|
expect(p.anilist, isNull);
|
|
expect(p.mal?.accessToken, connectedMal.accessToken);
|
|
expect((await _malStore.load(uuid))?.accessToken, connectedMal.accessToken);
|
|
expect(MalTracker.instance.client?.session.accessToken, connectedMal.accessToken);
|
|
p.dispose();
|
|
});
|
|
|
|
test('dispose after save cannot assign or erase a newer same-profile binding', () async {
|
|
const uuid = 'profile-save-race';
|
|
final staleSession = _session(TrackerService.mal, 'stale');
|
|
final freshSession = _session(TrackerService.mal, 'fresh');
|
|
final pipeline = _ControlledConnectPipeline(staleSession, pauseAfterSave: true);
|
|
final staleProvider = TrackersProvider.forTesting(connectPipeline: pipeline.call);
|
|
await _bindProfile(staleProvider, uuid);
|
|
final connect = _connect(staleProvider, TrackerService.mal);
|
|
await pipeline.beforeSave.future;
|
|
pipeline.releaseBeforeSave.complete();
|
|
await pipeline.afterSave.future;
|
|
|
|
staleProvider.dispose();
|
|
await _malStore.save(uuid, freshSession);
|
|
BaseSharedPreferencesService.resetForTesting();
|
|
final freshProvider = TrackersProvider();
|
|
await _bindProfile(freshProvider, uuid);
|
|
final freshBinding = MalTracker.instance.client;
|
|
pipeline.releaseAfterSave.complete();
|
|
|
|
expect(await connect, isFalse);
|
|
expect((await _malStore.load(uuid))?.accessToken, freshSession.accessToken);
|
|
expect(MalTracker.instance.client, same(freshBinding));
|
|
expect(MalTracker.instance.client?.session.accessToken, freshSession.accessToken);
|
|
expect(staleProvider.mal, isNull);
|
|
freshProvider.dispose();
|
|
});
|
|
});
|
|
}
|
|
|
|
void _resetTrackerBindings() {
|
|
MalTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
|
AnilistTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
|
SimklTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
|
TraktTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
|
}
|
|
|
|
TrackerAccountStore _store(TrackerService service) => switch (service) {
|
|
TrackerService.mal => _malStore,
|
|
TrackerService.anilist => _anilistStore,
|
|
TrackerService.simkl => _simklStore,
|
|
TrackerService.trakt => _traktStore,
|
|
};
|
|
|
|
TrackerSession _session(TrackerService service, String owner) => switch (service) {
|
|
TrackerService.mal => TrackerSession(
|
|
accessToken: '$owner-mal-at',
|
|
refreshToken: '$owner-mal-rt',
|
|
expiresAt: 2000000000,
|
|
createdAt: 1900000000,
|
|
username: owner,
|
|
),
|
|
TrackerService.anilist => TrackerSession(
|
|
accessToken: '$owner-anilist-at',
|
|
expiresAt: 2000000000,
|
|
createdAt: 1900000000,
|
|
username: owner,
|
|
),
|
|
TrackerService.simkl => TrackerSession(accessToken: '$owner-simkl-at', createdAt: 1900000000, username: owner),
|
|
TrackerService.trakt => TrackerSession(
|
|
accessToken: '$owner-trakt-at',
|
|
refreshToken: '$owner-trakt-rt',
|
|
expiresAt: 2000000000,
|
|
createdAt: 1900000000,
|
|
username: owner,
|
|
),
|
|
};
|
|
|
|
Future<bool> _connect(TrackersProvider provider, TrackerService service) => switch (service) {
|
|
TrackerService.mal => provider.connectMal(onCodeReady: (_) {}),
|
|
TrackerService.anilist => provider.connectAnilist(onCodeReady: (_) {}),
|
|
TrackerService.simkl => provider.connectSimkl(onCodeReady: (_) {}),
|
|
TrackerService.trakt => provider.connectTrakt(onCodeReady: (_) {}),
|
|
};
|
|
|
|
TrackerSession? _providerSession(TrackersProvider provider, TrackerService service) => switch (service) {
|
|
TrackerService.mal => provider.mal,
|
|
TrackerService.anilist => provider.anilist,
|
|
TrackerService.simkl => provider.simkl,
|
|
TrackerService.trakt => provider.trakt,
|
|
};
|
|
|
|
Object? _boundClient(TrackerService service) => switch (service) {
|
|
TrackerService.mal => MalTracker.instance.client,
|
|
TrackerService.anilist => AnilistTracker.instance.client,
|
|
TrackerService.simkl => SimklTracker.instance.client,
|
|
TrackerService.trakt => TraktTracker.instance.client,
|
|
};
|
|
|
|
TrackerSession? _boundSession(TrackerService service) => switch (service) {
|
|
TrackerService.mal => MalTracker.instance.client?.session,
|
|
TrackerService.anilist => AnilistTracker.instance.client?.session,
|
|
TrackerService.simkl => SimklTracker.instance.client?.session,
|
|
TrackerService.trakt => TraktTracker.instance.client?.session,
|
|
};
|
|
|
|
class _ControlledConnectPipeline {
|
|
_ControlledConnectPipeline(this.session, {this.pauseAfterSave = false});
|
|
|
|
final TrackerSession session;
|
|
final bool pauseAfterSave;
|
|
final beforeSave = Completer<void>();
|
|
final releaseBeforeSave = Completer<void>();
|
|
final afterSave = Completer<void>();
|
|
final releaseAfterSave = Completer<void>();
|
|
|
|
Future<bool> call({
|
|
required String logLabel,
|
|
required Future<TrackerSession?> Function() authorize,
|
|
required Future<TrackerSession> Function(TrackerSession raw) enrich,
|
|
required Future<void> Function(TrackerSession enriched) save,
|
|
required void Function(TrackerSession enriched) assign,
|
|
}) async {
|
|
beforeSave.complete();
|
|
await releaseBeforeSave.future;
|
|
await save(session);
|
|
afterSave.complete();
|
|
if (pauseAfterSave) await releaseAfterSave.future;
|
|
assign(session);
|
|
return true;
|
|
}
|
|
}
|