< Summary

Class:DCLServices.EmotesCatalog.EmotesBatchRequest
Assembly:EmotesCatalog
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesCatalog/EmotesBatchRequest.cs
Covered lines:7
Uncovered lines:77
Coverable lines:84
Total lines:208
Line coverage:8.3% (7 of 84)
Covered branches:0
Total branches:0
Covered methods:1
Total methods:9
Method coverage:11.1% (1 of 9)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
EmotesBatchRequest(...)0%110100%
Dispose()0%2100%
RequestOwnedEmotes(...)0%2100%
RequestOwnedEmotesAsync()0%90900%
FullListEmoteFetch()0%72800%
RequestEmote(...)0%2100%
RequestWearableBatchAsync()0%1101000%
FetchEmotes()0%1561200%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/DCLServices/EmotesCatalog/EmotesBatchRequest.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL;
 3using DCLServices.Lambdas;
 4using MainScripts.DCL.Helpers.Utils;
 5using System;
 6using System.Collections.Generic;
 7using System.Linq;
 8using System.Threading;
 9using UnityEngine.Pool;
 10
 11namespace DCLServices.EmotesCatalog
 12{
 13    public class EmotesBatchRequest : IEmotesRequestSource
 14    {
 15        [Serializable]
 16        public class OwnedEmotesRequestDto
 17        {
 18            // https://decentraland.github.io/catalyst-api-specs/#tag/Lambdas/operation/getEmotes
 19
 20            [Serializable]
 21            public class EmoteRequestDto
 22            {
 23                public string urn;
 24                public int amount;
 25                public IndividualData[] individualData;
 26
 27                [Serializable]
 28                public struct IndividualData
 29                {
 30                    // extended urn
 31                    public string id;
 32                }
 33            }
 34
 35            public EmoteRequestDto[] elements;
 36            public int totalAmount;
 37            public int pageSize;
 38            public int pageNum;
 39        }
 40
 41        public event Action<IReadOnlyList<WearableItem>> OnEmotesReceived;
 42        public event IEmotesRequestSource.OwnedEmotesReceived OnOwnedEmotesReceived;
 43        public event EmoteRejectedDelegate OnEmoteRejected;
 44
 45        private readonly ILambdasService lambdasService;
 46        private readonly ICatalyst catalyst;
 4047        private readonly HashSet<string> pendingRequests = new ();
 48        private readonly CancellationTokenSource cts;
 49        private readonly BaseVariable<FeatureFlag> featureFlags;
 50        private UniTaskCompletionSource<IReadOnlyList<WearableItem>> lastRequestSource;
 51
 052        private string assetBundlesUrl => featureFlags.Get().IsFeatureEnabled("ab-new-cdn") ? "https://ab-cdn.decentrala
 53
 4054        public EmotesBatchRequest(ILambdasService lambdasService, IServiceProviders serviceProviders, BaseVariable<Featu
 55        {
 4056            this.featureFlags = featureFlags;
 4057            this.lambdasService = lambdasService;
 4058            catalyst = serviceProviders.catalyst;
 4059            cts = new CancellationTokenSource();
 4060        }
 61
 62        public void Dispose()
 63        {
 064            cts.Cancel();
 065            cts.Dispose();
 066        }
 67
 68        public void RequestOwnedEmotes(string userId)
 69        {
 070            RequestOwnedEmotesAsync(userId).Forget();
 071        }
 72
 73        private async UniTask RequestOwnedEmotesAsync(string userId)
 74        {
 075            var url = $"{catalyst.lambdasUrl}/users/{userId}/emotes";
 76
 077            var requestedEmotes = new List<OwnedEmotesRequestDto.EmoteRequestDto>();
 78
 079            if (!await FullListEmoteFetch(userId, url, requestedEmotes))
 080                return;
 81
 082            PoolUtils.ListPoolRent<string> tempList = PoolUtils.RentList<string>();
 083            List<string> emoteUrns = tempList.GetList();
 84
 085            var urnToAmountMap = new Dictionary<string, int>();
 086            var idToExtendedUrn = new Dictionary<string, string>();
 87
 088            foreach (OwnedEmotesRequestDto.EmoteRequestDto emoteRequestDto in requestedEmotes)
 89            {
 090                emoteUrns.Add(emoteRequestDto.urn);
 091                urnToAmountMap[emoteRequestDto.urn] = emoteRequestDto.amount;
 092                idToExtendedUrn[emoteRequestDto.urn] = emoteRequestDto.individualData[0].id;
 93            }
 94
 095            IReadOnlyList<WearableItem> emotes = await FetchEmotes(emoteUrns, urnToAmountMap);
 96
 097            tempList.Dispose();
 98
 099            OnOwnedEmotesReceived?.Invoke(emotes, userId, idToExtendedUrn);
 0100        }
 101
 102        // This recursiveness is horrible, we should add proper pagination
 103        private async UniTask<bool> FullListEmoteFetch(string userId, string url, List<OwnedEmotesRequestDto.EmoteReques
 104        {
 0105            var requestedCount = 0;
 0106            var totalEmotes = 99999;
 0107            var pageNum = 1;
 108
 0109            while (requestedCount < totalEmotes)
 110            {
 0111                (string name, string value)[] queryParams = new List<(string name, string value)>
 112                {
 113                    ("pageNum", pageNum.ToString()),
 114                }.ToArray();
 115
 0116                (OwnedEmotesRequestDto response, bool success) result = await lambdasService.GetFromSpecificUrl<OwnedEmo
 117                    cancellationToken: cts.Token, urlEncodedParams: queryParams);
 118
 0119                if (!result.success) throw new Exception($"Fetching owned wearables failed! {url}\nAddress: {userId}");
 120
 0121                if (result.response.elements.Length <= 0 && requestedCount <= 0)
 122                {
 0123                    OnOwnedEmotesReceived?.Invoke(new List<WearableItem>(), userId, new Dictionary<string, string>());
 0124                    return false;
 125                }
 126
 0127                requestedCount += result.response.elements.Length;
 0128                totalEmotes = result.response.totalAmount;
 0129                pageNum++;
 0130                requestedEmotes.AddRange(result.response.elements);
 131            }
 132
 0133            return true;
 0134        }
 135
 136        public void RequestEmote(string emoteId)
 137        {
 0138            RequestWearableBatchAsync(emoteId).Forget();
 0139        }
 140
 141        private async UniTask RequestWearableBatchAsync(string id)
 142        {
 0143            pendingRequests.Add(id);
 0144            lastRequestSource ??= new UniTaskCompletionSource<IReadOnlyList<WearableItem>>();
 0145            UniTaskCompletionSource<IReadOnlyList<WearableItem>> sourceToAwait = lastRequestSource;
 146
 147            // we wait for the latest update possible so we buffer all requests into one
 0148            await UniTask.Yield(PlayerLoopTiming.PostLateUpdate, cts.Token);
 149
 150            IReadOnlyList<WearableItem> result;
 151
 0152            if (pendingRequests.Count > 0)
 153            {
 0154                lastRequestSource = null;
 155
 0156                List<string> tempList = ListPool<string>.Get();
 0157                tempList.AddRange(pendingRequests);
 0158                pendingRequests.Clear();
 159
 0160                result = await FetchEmotes(tempList);
 0161                ListPool<string>.Release(tempList);
 0162                sourceToAwait.TrySetResult(result);
 0163                OnEmotesReceived?.Invoke(result);
 0164            }
 165            else
 0166                await sourceToAwait.Task;
 0167        }
 168
 169        private async UniTask<IReadOnlyList<WearableItem>> FetchEmotes(
 170            IReadOnlyCollection<string> ids,
 171            Dictionary<string, int> urnToAmountMap = null)
 172        {
 173            // the copy of the list is intentional
 0174            var request = new LambdasEmotesCatalogService.WearableRequest { pointers = new List<string>(ids) };
 0175            var url = $"{catalyst.contentUrl}entities/active";
 176
 0177            (EmoteEntityDto[] response, bool success) response = await lambdasService.PostFromSpecificUrl<EmoteEntityDto
 178                url, url, request, cancellationToken: cts.Token);
 179
 0180            if (!response.success) throw new Exception($"Fetching wearables failed! {url}\n{string.Join("\n", request.po
 181
 0182            HashSet<string> receivedIds = HashSetPool<string>.Get();
 183
 0184            IEnumerable<WearableItem> wearables = response.response.Select(dto =>
 185            {
 0186                var contentUrl = $"{catalyst.contentUrl}contents/";
 0187                var wearableItem = dto.ToWearableItem(contentUrl);
 188
 0189                if (urnToAmountMap != null && urnToAmountMap.TryGetValue(dto.metadata.id, out int amount))
 0190                    wearableItem.amount = amount;
 191
 0192                wearableItem.baseUrl = contentUrl;
 0193                wearableItem.baseUrlBundles = assetBundlesUrl;
 0194                return wearableItem;
 195            });
 196
 0197            foreach (WearableItem wearableItem in wearables)
 0198                receivedIds.Add(wearableItem.id);
 199
 0200            foreach (string id in ids)
 0201                if (!receivedIds.Contains(id))
 0202                    OnEmoteRejected?.Invoke(id, "Empty response from content server");
 203
 0204            HashSetPool<string>.Release(receivedIds);
 0205            return wearables.ToList();
 0206        }
 207    }
 208}