using System; using System.Threading; using System.Threading.Tasks; using RudderSdk.Core.Abstractions; using RudderSdk.Core.Models.Auth; namespace RudderSdk.Core; /// /// Entry point of the Rudder SDK. Composes all feature services around one /// configuration () and owns the session /// lifecycle: every API call goes through here, a 401 triggers a single-flight /// token refresh and one transparent retry. /// public sealed class RudderClient { private readonly object _refreshGate = new(); private Task? _pendingRefresh; /// Authentication: device login, token refresh, logout. public AuthService Auth { get; } /// Player profile and wallets. public PlayerService Player { get; } /// Remote configuration values. public RemoteConfigService RemoteConfig { get; } /// Player key-value storage. public StorageService Storage { get; } /// Project key-value storage (global, shared across players). public ProjectStorageService ProjectStorage { get; } /// In-game stores and offer purchases. public StoresService Stores { get; } /// Leaderboards. public LeaderboardsService Leaderboards { get; } /// Player inventory. public InventoryService Inventory { get; } /// Battle pass progress and rewards. public BattlePassService BattlePass { get; } /// Global quests. public QuestsService Quests { get; } /// Scenario trigger. Execution lives on the server. public ScenarioService Scenario { get; } /// Pending scenario effects: subscriptions and completion callbacks. public EffectsService Effects { get; } internal RudderClientOptions Options { get; } internal IRudderTransport Transport => Options.Transport!; internal ITokenStore TokenStore => Options.TokenStore!; internal IDeviceIdProvider DeviceIdProvider => Options.DeviceIdProvider!; /// Project key from . public string ProjectKey => Options.ProjectKey!; /// Time source used by the effects client. public IClock Clock => Options.Clock ?? SystemClock.Instance; /// /// Creates the client. and /// are required; transport, /// token store and device id provider fall back to defaults. /// public RudderClient(RudderClientOptions options) { Options = options ?? throw new ArgumentNullException(nameof(options)); if (string.IsNullOrEmpty(Options.BaseUrl)) throw new ArgumentException("BaseUrl is required.", nameof(options)); if (string.IsNullOrEmpty(Options.ProjectKey)) throw new ArgumentException("ProjectKey is required.", nameof(options)); Options.Transport ??= new HttpClientTransport(Options.BaseUrl); Options.TokenStore ??= new InMemoryTokenStore(); Options.DeviceIdProvider ??= new GuidDeviceIdProvider(); Auth = new AuthService(this); Player = new PlayerService(this); RemoteConfig = new RemoteConfigService(this); Storage = new StorageService(this); ProjectStorage = new ProjectStorageService(this); Stores = new StoresService(this); Leaderboards = new LeaderboardsService(this); Inventory = new InventoryService(this); BattlePass = new BattlePassService(this); Quests = new QuestsService(this); Effects = new EffectsService(this); Scenario = new ScenarioService(this); } /// Pumps time-dependent services; call every frame. public void Update(float deltaTime) { Effects.Update(deltaTime); } internal Task SendAsync( string method, string path, TRequest? request, CancellationToken cancellationToken = default) { return SendWithRetryAsync(method, path, request, cancellationToken); } internal Task SendAsync( string method, string path, CancellationToken cancellationToken = default) { return SendWithRetryAsync(method, path, null, cancellationToken); } internal Task SendAsync( string method, string path, TRequest? request, CancellationToken cancellationToken = default) { return SendWithRetryAsync(method, path, request, cancellationToken); } internal Task SendAsync( string method, string path, CancellationToken cancellationToken = default) { return SendWithRetryAsync(method, path, null, cancellationToken); } private async Task SendWithRetryAsync( string method, string path, TRequest? request, CancellationToken cancellationToken) { try { return await Transport .SendAsync(method, path, request, TokenStore.GetAccessToken(), cancellationToken) .ConfigureAwait(false); } catch (RudderAuthException) { if (!await RefreshTokensAsync().ConfigureAwait(false)) throw; return await Transport .SendAsync(method, path, request, TokenStore.GetAccessToken(), cancellationToken) .ConfigureAwait(false); } } /// /// Single-flight refresh: concurrent 401s share one refresh request. /// Resolves to true when a new token pair was stored. On failure the tokens /// are cleared and subscribers are notified via /// . /// internal Task RefreshTokensAsync() { lock (_refreshGate) { if (_pendingRefresh != null) return _pendingRefresh; var task = DoRefreshTokensAsync(); _pendingRefresh = task; task.ContinueWith( _ => { lock (_refreshGate) { if (ReferenceEquals(_pendingRefresh, task)) _pendingRefresh = null; } }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); return task; } } private async Task DoRefreshTokensAsync() { var refreshToken = TokenStore.GetRefreshToken(); if (!string.IsNullOrEmpty(refreshToken)) { try { var response = await Transport .SendAsync( "POST", "/sdk/v1/authorization/refresh", new RefreshAccessTokenRequest { RefreshToken = refreshToken }, null, CancellationToken.None) .ConfigureAwait(false); if (response != null && !string.IsNullOrEmpty(response.AccessToken) && !string.IsNullOrEmpty(response.RefreshToken)) { TokenStore.SaveTokens(response.AccessToken, response.RefreshToken); return true; } } catch { } } TokenStore.Clear(); Auth.NotifySignedOut(); return false; } private sealed class SystemClock : IClock { public static readonly SystemClock Instance = new(); public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; } } internal sealed class EmptyResponse { }