< Summary

Class:LambdasEmotesCatalogService
Assembly:EmotesCatalog
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesCatalog/EmotesCatalogService/LambdasEmotesCatalogService.cs
Covered lines:92
Uncovered lines:125
Coverable lines:217
Total lines:478
Line coverage:42.3% (92 of 217)
Covered branches:0
Total branches:0
Covered methods:12
Total methods:26
Method coverage:46.1% (12 of 26)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
LambdasEmotesCatalogService(...)0%110100%
InitializeAsyncEmbeddedEmotes()0%8.034036.84%
Initialize()0%110100%
Dispose()0%2100%
TryGetLoadedEmote(...)0%2100%
RequestEmoteFromBuilderAsync()0%56700%
RequestOwnedEmotes(...)0%12300%
RequestOwnedEmotesAsync()0%20400%
RequestEmoteCollectionAsync()0%1101000%
RequestEmote(...)0%550100%
RequestEmotes(...)0%220100%
RequestEmoteAsync()0%11.119070.37%
RequestEmotesAsync()0%440100%
ForgetEmote(...)0%440100%
ForgetEmotes(...)0%6200%
TryGetOwnedUrn(...)0%2100%
GetEmbeddedEmotes()0%440100%
RequestEmoteCollectionInBuilderAsync()0%2101400%
DisposeAddressableCancellationToken()0%6200%
OnEmotesReceived(...)0%330100%
OnEmoteReceived(...)0%550100%
OnEmoteRejected(...)0%12300%
OnOwnedEmotesReceived(...)0%72800%
EmbedEmotes()0%330100%
GetBuilderDomainUrl()0%6200%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesCatalog/EmotesCatalogService/LambdasEmotesCatalogService.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL;
 3using DCL.Emotes;
 4using DCL.Helpers;
 5using DCL.Providers;
 6using DCLServices.EmotesCatalog;
 7using DCLServices.Lambdas;
 8using DCLServices.WearablesCatalogService;
 9using System;
 10using System.Collections.Generic;
 11using System.Threading;
 12using UnityEngine;
 13
 14public class LambdasEmotesCatalogService : IEmotesCatalogService
 15{
 16    [Serializable]
 17    internal class WearableRequest
 18    {
 19        public List<string> pointers;
 20    }
 21
 22    [Serializable]
 23    private class EmoteCollectionResponse
 24    {
 25        public EmoteEntityDto[] entities;
 26    }
 27
 6328    internal readonly Dictionary<string, WearableItem> emotes = new (new Dictionary<string, WearableItem>(), StringIgnor
 6329    internal readonly Dictionary<string, HashSet<Promise<WearableItem>>> promises = new (new Dictionary<string, HashSet<
 30        StringIgnoreCaseEqualityComparer.Default);
 6331    internal readonly Dictionary<string, int> emotesOnUse = new (new Dictionary<string, int>(), StringIgnoreCaseEquality
 6332    internal readonly Dictionary<string, HashSet<Promise<IReadOnlyList<WearableItem>>>> ownedEmotesPromisesByUser = new 
 33        new Dictionary<string, HashSet<Promise<IReadOnlyList<WearableItem>>>>(), StringIgnoreCaseEqualityComparer.Defaul
 34
 35    private readonly IEmotesRequestSource emoteSource;
 36    private readonly IAddressableResourceProvider addressableResourceProvider;
 37    private readonly ICatalyst catalyst;
 38    private readonly ILambdasService lambdasService;
 39    private readonly DataStore dataStore;
 40    private readonly KernelConfig kernelConfig;
 6341    private readonly Dictionary<string, string> ownedUrns = new (new Dictionary<string, string>(), StringIgnoreCaseEqual
 42
 43    private EmbeddedEmotesSO embeddedEmotesSo;
 44    private CancellationTokenSource addressableCts;
 6345    private int retryCount = 3;
 46
 047    private string assetBundlesUrl => dataStore.featureFlags.flags.Get().IsFeatureEnabled("ab-new-cdn") ? "https://ab-cd
 48
 6349    public LambdasEmotesCatalogService(IEmotesRequestSource emoteSource,
 50        IAddressableResourceProvider addressableResourceProvider,
 51        ICatalyst catalyst,
 52        ILambdasService lambdasService,
 53        DataStore dataStore,
 54        KernelConfig kernelConfig)
 55    {
 6356        this.emoteSource = emoteSource;
 6357        this.addressableResourceProvider = addressableResourceProvider;
 6358        this.catalyst = catalyst;
 6359        this.lambdasService = lambdasService;
 6360        this.dataStore = dataStore;
 6361        this.kernelConfig = kernelConfig;
 6362    }
 63
 64    private async UniTaskVoid InitializeAsyncEmbeddedEmotes()
 65    {
 66        try
 67        {
 3468            addressableCts = new CancellationTokenSource();
 5669            embeddedEmotesSo = await addressableResourceProvider.GetAddressable<EmbeddedEmotesSO>("EmbeddedEmotes.asset"
 3470        }
 071        catch (OperationCanceledException) { return; }
 072        catch (Exception e)
 73        {
 074            retryCount--;
 75
 076            if (retryCount < 0)
 77            {
 078                embeddedEmotesSo = ScriptableObject.CreateInstance<EmbeddedEmotesSO>();
 079                embeddedEmotesSo.Clear();
 080                throw new Exception("Embedded Emotes retry limit reached, they wont work correctly. Please check the Ess
 81            }
 82
 083            Debug.LogWarning("Retrying embedded emotes addressables async request...");
 084            DisposeAddressableCancellationToken();
 085            InitializeAsyncEmbeddedEmotes().Forget();
 086        }
 87
 3488        EmbedEmotes();
 3489    }
 90
 91    public void Initialize()
 92    {
 3493        InitializeAsyncEmbeddedEmotes().Forget();
 3494        emoteSource.OnEmotesReceived += OnEmotesReceived;
 3495        emoteSource.OnEmoteRejected += OnEmoteRejected;
 3496        emoteSource.OnOwnedEmotesReceived += OnOwnedEmotesReceived;
 3497    }
 98
 99    public void Dispose()
 100    {
 0101        emoteSource.OnEmotesReceived -= OnEmotesReceived;
 0102        emoteSource.OnEmoteRejected -= OnEmoteRejected;
 0103        emoteSource.OnOwnedEmotesReceived -= OnOwnedEmotesReceived;
 0104        DisposeAddressableCancellationToken();
 0105    }
 106
 107    public bool TryGetLoadedEmote(string id, out WearableItem emote)
 108    {
 0109        return emotes.TryGetValue(id, out emote);
 110    }
 111
 112    public async UniTask<WearableItem> RequestEmoteFromBuilderAsync(string emoteId, CancellationToken cancellationToken)
 113    {
 0114        string domain = GetBuilderDomainUrl();
 0115        string url = $"{domain}/items/{emoteId}/";
 0116        string templateUrl = $"{domain}/items/{emoteId}/";
 117
 118        try
 119        {
 0120            (WearableItemResponseFromBuilder response, bool success) = await lambdasService.GetFromSpecificUrl<WearableI
 121                templateUrl, url,
 122                isSigned: true,
 123                cancellationToken: cancellationToken);
 124
 0125            if (!success)
 0126                throw new Exception($"The request of wearables from builder '{emoteId}' failed!");
 127
 0128            WearableItem wearable = response.data.ToWearableItem(
 129                $"{domain}/storage/contents/",
 130                assetBundlesUrl);
 131
 0132            if (!wearable.IsEmote()) return null;
 133
 0134            OnEmoteReceived(wearable);
 135
 0136            return wearable;
 137        }
 0138        catch (UnityWebRequestException ex)
 139        {
 0140            if (ex.ResponseCode == 404)
 0141                Debug.LogWarning($"Emote with id: {emoteId} does not exist");
 142            else
 0143                Debug.LogException(ex);
 144
 0145            return null;
 146        }
 147        catch (Exception e)
 148        {
 0149            Debug.LogException(e);
 0150            return null;
 151        }
 0152    }
 153
 154    public Promise<IReadOnlyList<WearableItem>> RequestOwnedEmotes(string userId)
 155    {
 0156        var promise = new Promise<IReadOnlyList<WearableItem>>();
 157
 0158        if (!ownedEmotesPromisesByUser.ContainsKey(userId) || ownedEmotesPromisesByUser[userId] == null)
 0159            ownedEmotesPromisesByUser[userId] = new HashSet<Promise<IReadOnlyList<WearableItem>>>();
 160
 0161        ownedEmotesPromisesByUser[userId].Add(promise);
 162
 0163        emoteSource.RequestOwnedEmotes(userId);
 164
 0165        return promise;
 166    }
 167
 168    public async UniTask<IReadOnlyList<WearableItem>> RequestOwnedEmotesAsync(string userId, CancellationToken ct = defa
 169    {
 0170        var promise = RequestOwnedEmotes(userId);
 171
 172        try
 173        {
 0174            ct.ThrowIfCancellationRequested();
 0175            await promise.WithCancellation(ct);
 0176        }
 0177        catch (OperationCanceledException e) { return null; }
 178
 0179        return promise.value;
 0180    }
 181
 182    public async UniTask<IReadOnlyList<WearableItem>> RequestEmoteCollectionAsync(IEnumerable<string> collectionIds,
 183        CancellationToken cancellationToken, List<WearableItem> emoteBuffer = null)
 184    {
 0185        List<WearableItem> emotes = emoteBuffer ?? new List<WearableItem>();
 0186        var templateURL = $"{catalyst.contentUrl}entities/active/collections/:collectionId";
 187
 0188        foreach (string collectionId in collectionIds)
 189        {
 0190            string url = templateURL.Replace(":collectionId", collectionId);
 191
 0192            (EmoteCollectionResponse response, bool success) = await lambdasService.GetFromSpecificUrl<EmoteCollectionRe
 193                templateURL, url,
 194                cancellationToken: cancellationToken);
 195
 0196            if (!success)
 0197                throw new Exception($"The request for collection of emotes '{collectionId}' failed!");
 198
 0199            foreach (EmoteEntityDto dto in response.entities)
 200            {
 0201                var contentUrl = $"{catalyst.contentUrl}contents/";
 0202                var wearable = dto.ToWearableItem(contentUrl);
 0203                wearable.baseUrl = contentUrl;
 0204                wearable.baseUrlBundles = assetBundlesUrl;
 0205                emotes.Add(wearable);
 206            }
 0207        }
 208
 0209        OnEmotesReceived(emotes);
 210
 0211        return emotes;
 0212    }
 213
 214    public Promise<WearableItem> RequestEmote(string id)
 215    {
 42216        var promise = new Promise<WearableItem>();
 217
 42218        if (!emotesOnUse.ContainsKey(id))
 21219            emotesOnUse[id] = 0;
 220
 42221        emotesOnUse[id]++;
 222
 42223        if (emotes.TryGetValue(id, out var emote))
 224        {
 3225            promise.Resolve(emote);
 3226            return promise;
 227        }
 228
 39229        if (!promises.ContainsKey(id) || promises[id] == null)
 21230            promises[id] = new HashSet<Promise<WearableItem>>();
 231
 39232        promises[id].Add(promise);
 233
 39234        emoteSource.RequestEmote(id);
 235
 39236        return promise;
 237    }
 238
 239    public List<Promise<WearableItem>> RequestEmotes(IList<string> ids)
 240    {
 6241        List<Promise<WearableItem>> requestedPromises = new List<Promise<WearableItem>>(ids.Count);
 242
 38243        for (int i = 0; i < ids.Count; i++)
 244        {
 13245            string id = ids[i];
 13246            requestedPromises.Add(RequestEmote(id));
 247        }
 248
 6249        return requestedPromises;
 250    }
 251
 252    public async UniTask<WearableItem> RequestEmoteAsync(string id, CancellationToken ct = default)
 253    {
 254        const int TIMEOUT = 45;
 19255        CancellationTokenSource timeoutCTS = new CancellationTokenSource();
 19256        var timeout = timeoutCTS.CancelAfterSlim(TimeSpan.FromSeconds(TIMEOUT));
 19257        ct.ThrowIfCancellationRequested();
 19258        Promise<WearableItem> promise = RequestEmote(id);
 259
 260        try
 261        {
 19262            var linkedCt = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCTS.Token);
 51263            await promise.WithCancellation(linkedCt.Token);
 3264        }
 0265        catch (UnityWebRequestException ex)
 266        {
 0267            Debug.LogWarning($"Emote with id:{id} does not exist or connection failed");
 0268            return null;
 269        }
 0270        catch (PromiseException ex)
 271        {
 0272            Debug.LogWarning($"Emote with id:{id} was rejected");
 0273            return null;
 274        }
 16275        catch (OperationCanceledException ex)
 276        {
 16277            if (promises.ContainsKey(id))
 278            {
 16279                promises[id].Remove(promise);
 280
 16281                if (promises[id].Count == 0)
 6282                    promises.Remove(id);
 283            }
 284
 16285            return null;
 286        }
 287        catch (Exception ex)
 288        {
 0289            Debug.LogException(ex);
 0290            return null;
 291        }
 292        finally
 293        {
 19294            timeout?.Dispose();
 19295            timeoutCTS?.Dispose();
 296        }
 297
 3298        return promise.value;
 19299    }
 300
 301    public async UniTask<IReadOnlyList<WearableItem>> RequestEmotesAsync(IList<string> ids, CancellationToken ct = defau
 302    {
 6303        ct.ThrowIfCancellationRequested();
 304
 305        try
 306        {
 18307            var tasks = ids.Select(x => RequestEmoteAsync(x, ct));
 16308            WearableItem[] result = await UniTask.WhenAll(tasks).AttachExternalCancellation(ct);
 1309            return result;
 310        }
 10311        catch (OperationCanceledException) { return null; }
 6312    }
 313
 314    public void ForgetEmote(string id)
 315    {
 3316        if (emotesOnUse.ContainsKey(id))
 317        {
 2318            emotesOnUse[id]--;
 319
 2320            if (emotesOnUse[id] > 0) //We are still using this emote
 1321                return;
 322
 1323            emotesOnUse.Remove(id);
 324        }
 325
 2326        if (!emotes.TryGetValue(id, out WearableItem emote))
 1327            return;
 328
 1329        emotes.Remove(id);
 1330    }
 331
 332    public void ForgetEmotes(IList<string> ids)
 333    {
 0334        for (int i = 0; i < ids.Count; i++)
 335        {
 0336            string id = ids[i];
 0337            ForgetEmote(id);
 338        }
 0339    }
 340
 341    public bool TryGetOwnedUrn(string shortenedUrn, out string extendedUrn) =>
 0342        ownedUrns.TryGetValue(shortenedUrn, out extendedUrn);
 343
 344    public async UniTask<EmbeddedEmotesSO> GetEmbeddedEmotes()
 345    {
 11346        if (embeddedEmotesSo == null)
 198347            await UniTask.WaitUntil(() => embeddedEmotesSo != null);
 348
 11349        return embeddedEmotesSo;
 11350    }
 351
 352    public async UniTask<IReadOnlyList<WearableItem>> RequestEmoteCollectionInBuilderAsync(IEnumerable<string> collectio
 353        CancellationToken cancellationToken, List<WearableItem> emoteBuffer = null)
 354    {
 0355        string domain = GetBuilderDomainUrl();
 0356        var emotes = emoteBuffer ?? new List<WearableItem>();
 357
 0358        var queryParams = new[]
 359        {
 360            ("page", "1"),
 361            ("limit", "5000"),
 362        };
 363
 0364        foreach (string collectionId in collectionIds)
 365        {
 0366            var url = $"{domain}/collections/{collectionId}";
 0367            var templateUrl = $"{domain}/collections/:collectionId/items/";
 368
 0369            (WearableCollectionResponseFromBuilder response, bool success) = await lambdasService.GetFromSpecificUrl<Wea
 370                templateUrl, url,
 371                cancellationToken: cancellationToken,
 372                isSigned: true,
 373                urlEncodedParams: queryParams);
 374
 0375            if (!success)
 0376                throw new Exception($"The request for collection of emotes '{collectionId}' failed!");
 377
 0378            if (response.data?.results == null) continue;
 379
 0380            foreach (BuilderWearable bw in response.data.results)
 381            {
 0382                var wearable = bw.ToWearableItem($"{domain}/storage/contents/",
 383                    assetBundlesUrl);
 0384                if (!wearable.IsEmote()) continue;
 0385                emotes.Add(wearable);
 386            }
 0387        }
 388
 0389        OnEmotesReceived(emotes);
 390
 0391        return emotes;
 0392    }
 393
 394    private void DisposeAddressableCancellationToken()
 395    {
 0396        if (addressableCts == null) return;
 0397        addressableCts.Cancel();
 0398        addressableCts.Dispose();
 0399        addressableCts = null;
 0400    }
 401
 402    private void OnEmotesReceived(IEnumerable<WearableItem> receivedEmotes)
 403    {
 24404        foreach (var t in receivedEmotes)
 6405            OnEmoteReceived(t);
 6406    }
 407
 408    private void OnEmoteReceived(WearableItem emote)
 409    {
 6410        if (!emotesOnUse.ContainsKey(emote.id) || emotesOnUse[emote.id] <= 0)
 1411            return;
 412
 5413        emotes[emote.id] = emote;
 414
 5415        if (promises.TryGetValue(emote.id, out var emotePromises))
 416        {
 22417            foreach (Promise<WearableItem> promise in emotePromises)
 6418                promise.Resolve(emote);
 419
 5420            promises.Remove(emote.id);
 421        }
 5422    }
 423
 424    private void OnEmoteRejected(string emoteId, string errorMessage)
 425    {
 0426        if (!promises.TryGetValue(emoteId, out var setOfPromises)) return;
 427
 0428        foreach (var promise in setOfPromises)
 0429            promise.Reject(errorMessage);
 430
 0431        promises.Remove(emoteId);
 0432        emotesOnUse.Remove(emoteId);
 0433        emotes.Remove(emoteId);
 0434    }
 435
 436    private void OnOwnedEmotesReceived(IReadOnlyList<WearableItem> receivedEmotes, string userId,
 437        IReadOnlyDictionary<string, string> extendedUrns)
 438    {
 0439        foreach ((string shortenedUrn, string extendedUrn) in extendedUrns)
 0440            ownedUrns[shortenedUrn] = extendedUrn;
 441
 0442        if (!ownedEmotesPromisesByUser.TryGetValue(userId, out HashSet<Promise<IReadOnlyList<WearableItem>>> ownedEmotes
 0443            ownedEmotesPromises = new HashSet<Promise<IReadOnlyList<WearableItem>>>();
 444
 445        //Update emotes on use
 0446        foreach (var emote in receivedEmotes)
 447        {
 0448            emotesOnUse.TryAdd(emote.id, 0);
 0449            emotesOnUse[emote.id] += ownedEmotesPromises.Count;
 450        }
 451
 0452        OnEmotesReceived(receivedEmotes);
 453
 454        //Resolve ownedEmotesPromise
 0455        ownedEmotesPromisesByUser.Remove(userId);
 456
 0457        foreach (Promise<IReadOnlyList<WearableItem>> promise in ownedEmotesPromises)
 0458            promise.Resolve(receivedEmotes);
 0459    }
 460
 461    private void EmbedEmotes()
 462    {
 822463        foreach (EmbeddedEmote embeddedEmote in embeddedEmotesSo.GetAllEmotes())
 464        {
 377465            emotes[embeddedEmote.id] = embeddedEmote;
 377466            emotesOnUse[embeddedEmote.id] = 5000;
 467        }
 34468    }
 469
 470    private string GetBuilderDomainUrl()
 471    {
 0472        string domain = kernelConfig.Get().builderUrl;
 473
 0474        if (string.IsNullOrEmpty(domain))
 0475            domain = "https://builder-api.decentraland.org/v1";
 0476        return domain;
 477    }
 478}