1- using Cysharp . Threading . Tasks ;
1+ using Cysharp . Threading . Tasks ;
22using DCL . Diagnostics ;
3+ using DCL . FeatureFlags ;
34using DCL . Ipfs ;
45using System ;
56using System . Collections . Generic ;
1213using Newtonsoft . Json ;
1314using Utility ;
1415using Utility . Json ;
16+ using Utility . Times ;
1517
1618namespace DCL . Backpack . AvatarSection . Outfits . Repository
1719{
@@ -26,18 +28,48 @@ public class OutfitsRepository
2628 private static readonly JsonSerializerSettings SERIALIZER_SETTINGS = new ( )
2729 { Converters = new List < JsonConverter > { new ColorJsonConverter ( ) } } ;
2830
31+ private const int DEFAULT_DEPLOY_WINDOW_IN_SECONDS = 15 ;
32+
2933 private readonly PublishIpfsEntityCommand publishIpfsEntityCommand ;
3034 private readonly INftNamesProvider nftNamesProvider ;
3135
36+ private ulong passedTimeSinceLastDeployment = 0 ;
37+ private ulong lastDeployTimestampInSeconds = 0 ;
38+
39+ private UniTaskCompletionSource ? currentResolutionTask ;
40+ private List < OutfitItem > ? currentOutfits ;
41+ private int currentVersion ;
42+
3243 public OutfitsRepository ( PublishIpfsEntityCommand publishIpfsEntityCommand ,
3344 INftNamesProvider nftNamesProvider )
3445 {
3546 this . publishIpfsEntityCommand = publishIpfsEntityCommand ;
3647 this . nftNamesProvider = nftNamesProvider ;
3748 }
3849
50+ private static int deployWindowInSeconds
51+ {
52+ get
53+ {
54+ if ( FeatureFlagsConfiguration . Instance . TryGetJsonPayload ( FeatureFlagsStrings . OUTFITS_DEPLOY_WINDOW ,
55+ "deploy_window_in_seconds" ,
56+ out OutfitsDeployWindowConfig ? config ) && config . HasValue && config . Value . DeployWindowInSeconds > 0 )
57+ return config . Value . DeployWindowInSeconds ;
58+
59+ return DEFAULT_DEPLOY_WINDOW_IN_SECONDS ;
60+ }
61+ }
62+
63+ [ Serializable ]
64+ private struct OutfitsDeployWindowConfig
65+ {
66+ [ JsonProperty ( "deploy_window_in_seconds" ) ] public int DeployWindowInSeconds ;
67+ }
68+
3969 /// <summary>
4070 /// Deploys the complete set of outfits for a user to the Catalyst network.
71+ /// Multiple rapid calls are coalesced: only the latest state is deployed,
72+ /// after a 15-second delay that respects the backend rate limit.
4173 /// </summary>
4274 public async UniTask SetAsync ( Profile ? profile , List < OutfitItem > outfits , CancellationToken ct )
4375 {
@@ -47,33 +79,69 @@ public async UniTask SetAsync(Profile? profile, List<OutfitItem> outfits, Cancel
4779 if ( string . IsNullOrEmpty ( profile ? . UserId ) )
4880 throw new ArgumentException ( "Cannot save outfits for a user with an empty UserId" ) ;
4981
50- INftNamesProvider . PaginatedNamesResponse namesForExtraSlots = await nftNamesProvider . GetAsync ( new Web3Address ( profile . UserId ) , 1 , 1 , ct ) ;
82+ currentOutfits = outfits ;
83+ currentVersion ++ ;
84+ var deployWindow = ( ulong ) deployWindowInSeconds ;
5185
52- var metadata = new OutfitsMetadata
86+ if ( currentResolutionTask != null )
5387 {
54- outfits = outfits , namesForExtraSlots = namesForExtraSlots . Names . Count > 0
55- ? new List < string >
56- {
57- namesForExtraSlots . Names [ 0 ] ,
58- }
59- : new List < string > ( ) ,
60- } ;
88+ await UniTask . WhenAny ( currentResolutionTask . Task , UniTask . WaitUntilCanceled ( ct ) ) ;
89+ return ;
90+ }
6191
62- var outfitsEntity = new OutfitsEntity ( string . Empty , metadata )
92+ currentResolutionTask = new UniTaskCompletionSource ( ) ;
93+
94+ try
6395 {
64- version = OutfitsEntity . DEFAULT_VERSION , pointers = new [ ]
96+ passedTimeSinceLastDeployment = Math . Clamp ( ( DateTime . UtcNow . UnixTimeAsMilliseconds ( ) / 1000 ) - lastDeployTimestampInSeconds , 0 , deployWindow ) ;
97+
98+ int localVersion ;
99+
100+ do
65101 {
66- $ "{ profile . UserId } :outfits",
67- } ,
68- timestamp = DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) , type = IpfsRealmEntityType . Outfits . ToEntityString ( ) , content = Array . Empty < ContentDefinition > ( ) ,
69- } ;
102+ localVersion = currentVersion ;
103+ List < OutfitItem > localOutfits = currentOutfits ! ;
104+
105+ await UniTask . Delay ( TimeSpan . FromSeconds ( deployWindow - ( double ) passedTimeSinceLastDeployment ) , cancellationToken : ct ) ;
106+
107+ INftNamesProvider . PaginatedNamesResponse namesForExtraSlots = await nftNamesProvider . GetAsync ( new Web3Address ( profile . UserId ) , 1 , 1 , ct ) ;
108+
109+ var metadata = new OutfitsMetadata
110+ {
111+ outfits = localOutfits , namesForExtraSlots = namesForExtraSlots . Names . Count > 0
112+ ? new List < string >
113+ {
114+ namesForExtraSlots . Names [ 0 ] ,
115+ }
116+ : new List < string > ( ) ,
117+ } ;
70118
71- try { await publishIpfsEntityCommand . ExecuteAsync ( outfitsEntity , ct , SERIALIZER_SETTINGS ) ; }
119+ var outfitsEntity = new OutfitsEntity ( string . Empty , metadata )
120+ {
121+ version = OutfitsEntity . DEFAULT_VERSION , pointers = new [ ]
122+ {
123+ $ "{ profile . UserId } :outfits",
124+ } ,
125+ timestamp = DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) , type = IpfsRealmEntityType . Outfits . ToEntityString ( ) , content = Array . Empty < ContentDefinition > ( ) ,
126+ } ;
127+
128+ await publishIpfsEntityCommand . ExecuteAsync ( outfitsEntity , ct , SERIALIZER_SETTINGS ) ;
129+ passedTimeSinceLastDeployment = 0 ;
130+ }
131+ while ( localVersion != currentVersion ) ;
132+
133+ currentResolutionTask . TrySetResult ( ) ;
134+ }
72135 catch ( Exception e )
73136 {
74- ReportHub . LogException ( e , ReportCategory . OUTFITS ) ;
137+ currentResolutionTask . TrySetException ( e ) ;
75138 throw ;
76139 }
140+ finally
141+ {
142+ currentResolutionTask = null ;
143+ lastDeployTimestampInSeconds = DateTime . UtcNow . UnixTimeAsMilliseconds ( ) / 1000 ;
144+ }
77145 }
78146 }
79147}
0 commit comments