From d2d687bb585098f43c3ab78ebc6b27bcbd58f1a0 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Thu, 4 Apr 2024 02:06:39 +0200 Subject: [PATCH 1/3] feature: change payloads of raw Changes how the raw endpoints provide their data. Rather than providing serialized versions of the endpoints we send the raw binary payloads fix #41 --- .../Controllers/ArrowHeadController.cs | 39 ++++--- .../wwwroot/Helldivers-2-API.json | 38 +++---- .../wwwroot/Helldivers-2-API_arrowhead.json | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 15 +-- .../Facades/ArrowHeadFacade.cs | 29 ----- .../Localization/CultureDictionary.cs | 2 +- .../Storage/ArrowHead/ArrowHeadStore.cs | 100 ++++++++++++++++++ .../Storage/ArrowHead/AssignmentStore.cs | 39 ------- .../Storage/ArrowHead/NewsFeedStore.cs | 39 ------- .../Storage/ArrowHead/WarIdStore.cs | 10 -- .../Storage/ArrowHead/WarInfoStore.cs | 10 -- .../Storage/ArrowHead/WarStatusStore.cs | 27 ----- .../Storage/ArrowHead/WarSummaryStore.cs | 10 -- src/Helldivers-2-Core/StorageFacade.cs | 49 ++++++++- .../Hosted/ArrowHeadSyncService.cs | 59 ++--------- .../Services/ArrowHeadApiService.cs | 67 +++++------- 16 files changed, 224 insertions(+), 311 deletions(-) delete mode 100644 src/Helldivers-2-Core/Facades/ArrowHeadFacade.cs create mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/AssignmentStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/NewsFeedStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/WarIdStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/WarInfoStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/WarStatusStore.cs delete mode 100644 src/Helldivers-2-Core/Storage/ArrowHead/WarSummaryStore.cs diff --git a/src/Helldivers-2-API/Controllers/ArrowHeadController.cs b/src/Helldivers-2-API/Controllers/ArrowHeadController.cs index 9889438..426fc93 100644 --- a/src/Helldivers-2-API/Controllers/ArrowHeadController.cs +++ b/src/Helldivers-2-API/Controllers/ArrowHeadController.cs @@ -1,5 +1,4 @@ -using Helldivers.Core.Contracts; -using Helldivers.Core.Contracts.Collections; +using Helldivers.Core.Storage.ArrowHead; using Helldivers.Models.ArrowHead; using Microsoft.AspNetCore.Mvc; @@ -14,55 +13,55 @@ public static class ArrowHeadController /// Returns the currently active war season ID. /// [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task WarId(HttpContext context, IStore store) + public static async Task WarId(HttpContext context, ArrowHeadStore store) { - var result = await store.Get(context.RequestAborted); + var warId = await store.GetWarId(context.RequestAborted); - return Results.Ok(result); + return Results.Bytes(warId, contentType: "application/json"); } /// /// Get a snapshot of the current war status. /// [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task Status(HttpContext context, IStore store) + public static async Task Status(HttpContext context, ArrowHeadStore store) { - var status = await store.Get(context.RequestAborted); + var status = await store.GetWarStatus(context.RequestAborted); - return Results.Ok(status); + return Results.Bytes(status, contentType: "application/json"); } /// /// Gets the current war info. /// [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task WarInfo(HttpContext context, IStore store) + public static async Task WarInfo(HttpContext context, ArrowHeadStore store) { - var info = await store.Get(context.RequestAborted); + var info = await store.GetWarInfo(context.RequestAborted); - return Results.Ok(info); + return Results.Bytes(info, contentType: "application/json"); } /// /// Gets the current war info. /// [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task Summary(HttpContext context, IStore store) + public static async Task Summary(HttpContext context, ArrowHeadStore store) { - var summary = await store.Get(context.RequestAborted); + var summary = await store.GetWarSummary(context.RequestAborted); - return Results.Ok(summary); + return Results.Bytes(summary, contentType: "application/json"); } /// /// Retrieves a list of news messages from Super Earth. /// [ProducesResponseType>(StatusCodes.Status200OK)] - public static async Task NewsFeed(HttpContext context, IStore store) + public static async Task NewsFeed(HttpContext context, ArrowHeadStore store) { - var items = await store.AllAsync(context.RequestAborted); + var items = await store.GetNewsFeeds(context.RequestAborted); - return Results.Ok(items); + return Results.Bytes(items, contentType: "application/json"); } /// @@ -70,10 +69,10 @@ public static async Task NewsFeed(HttpContext context, IStore [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - public static async Task Assignments(HttpContext context, IStore store) + public static async Task Assignments(HttpContext context, ArrowHeadStore store) { - var assignments = await store.AllAsync(context.RequestAborted); + var assignments = await store.GetAssignments(context.RequestAborted); - return Results.Ok(assignments); + return Results.Bytes(assignments, contentType: "application/json"); } } diff --git a/src/Helldivers-2-API/wwwroot/Helldivers-2-API.json b/src/Helldivers-2-API/wwwroot/Helldivers-2-API.json index 5ce2793..c9cdd3d 100644 --- a/src/Helldivers-2-API/wwwroot/Helldivers-2-API.json +++ b/src/Helldivers-2-API/wwwroot/Helldivers-2-API.json @@ -15,7 +15,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -46,7 +46,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -77,7 +77,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -108,7 +108,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -139,7 +139,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -173,7 +173,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -210,7 +210,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -241,7 +241,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -275,7 +275,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -318,7 +318,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -352,7 +352,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -395,7 +395,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -429,7 +429,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -472,7 +472,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -506,7 +506,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -549,7 +549,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -583,7 +583,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -618,7 +618,7 @@ }, { "url": "/", - "description": "The dotnet helldivers server" + "description": "The development server" } ], "get": { @@ -1801,4 +1801,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Helldivers-2-API/wwwroot/Helldivers-2-API_arrowhead.json b/src/Helldivers-2-API/wwwroot/Helldivers-2-API_arrowhead.json index 668037b..87ffedd 100644 --- a/src/Helldivers-2-API/wwwroot/Helldivers-2-API_arrowhead.json +++ b/src/Helldivers-2-API/wwwroot/Helldivers-2-API_arrowhead.json @@ -869,4 +869,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Helldivers-2-Core/Extensions/ServiceCollectionExtensions.cs b/src/Helldivers-2-Core/Extensions/ServiceCollectionExtensions.cs index e24cf43..884ace8 100644 --- a/src/Helldivers-2-Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Helldivers-2-Core/Extensions/ServiceCollectionExtensions.cs @@ -6,7 +6,6 @@ using Helldivers.Core.Storage.ArrowHead; using Helldivers.Core.Storage.Steam; using Helldivers.Core.Storage.V1; -using Helldivers.Models.ArrowHead; using Helldivers.Models.V1; using Microsoft.Extensions.DependencyInjection; @@ -35,20 +34,8 @@ public static IServiceCollection AddHelldivers(this IServiceCollection services) /// public static IServiceCollection AddArrowHeadStores(this IServiceCollection services) { - // Register facade for all stores below - services.AddSingleton(); - // Register all stores - services.AddSingleton, WarInfoStore>(); - services.AddSingleton(); - services.AddSingleton>(provider => provider.GetRequiredService()); - services.AddSingleton, WarSummaryStore>(); - services.AddSingleton(); - services.AddSingleton>(provider => - provider.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton>(provider => provider.GetRequiredService()); - services.AddSingleton, WarIdStore>(); + services.AddSingleton(); return services; } diff --git a/src/Helldivers-2-Core/Facades/ArrowHeadFacade.cs b/src/Helldivers-2-Core/Facades/ArrowHeadFacade.cs deleted file mode 100644 index 3967392..0000000 --- a/src/Helldivers-2-Core/Facades/ArrowHeadFacade.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Helldivers.Core.Contracts; -using Helldivers.Core.Storage.ArrowHead; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Facades; - -/// -/// Handles dispatching incoming data to all stores for ArrowHead. -/// -public sealed class ArrowHeadFacade( - IStore warIdStore, - IStore warInfoStore, - WarStatusStore warStatusStore, - IStore warSummaryStore, - NewsFeedStore newsFeedStore, - AssignmentStore assignmentStore -) -{ - /// - public async ValueTask UpdateStores(WarId warId, WarInfo warInfo, Dictionary warStatuses, WarSummary warSummary, Dictionary> newsFeeds, Dictionary> assignments) - { - await warIdStore.SetStore(warId); - await warInfoStore.SetStore(warInfo); - await warStatusStore.SetStore(warStatuses); - await warSummaryStore.SetStore(warSummary); - await newsFeedStore.SetStore(newsFeeds); - await assignmentStore.SetStore(assignments); - } -} diff --git a/src/Helldivers-2-Core/Localization/CultureDictionary.cs b/src/Helldivers-2-Core/Localization/CultureDictionary.cs index b17b106..2045035 100644 --- a/src/Helldivers-2-Core/Localization/CultureDictionary.cs +++ b/src/Helldivers-2-Core/Localization/CultureDictionary.cs @@ -6,7 +6,7 @@ namespace Helldivers.Core.Localization; /// Specialized version of that intelligently maps as /// keys (eg, if "en-UK" does not exist but "en-US" it'll match). /// -public class CultureDictionary where T : class +public class CultureDictionary { private readonly Dictionary _items; diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs new file mode 100644 index 0000000..6ef2ed3 --- /dev/null +++ b/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs @@ -0,0 +1,100 @@ +using Helldivers.Core.Localization; +using Helldivers.Models.ArrowHead; + +namespace Helldivers.Core.Storage.ArrowHead; + +/// +/// Dedicated storage for the ArrowHead payloads, as we want to be able to return those byte-by-byte. +/// +public sealed class ArrowHeadStore +{ + private Memory _warId; + private Memory _warInfo; + private Memory _warSummary; + private CultureDictionary> _statuses; + private CultureDictionary> _feeds; + private CultureDictionary> _assignments; + private readonly TaskCompletionSource _syncState = new(); + + /// + /// Updates the with the updated raw values. + /// + public void UpdateRawStore( + Memory warId, + Memory warInfo, + Memory warSummary, + IEnumerable>> statuses, + IEnumerable>> feeds, + IEnumerable>> assignments + ) + { + _warId = warId; + _warInfo = warInfo; + _warSummary = warSummary; + _statuses = new(statuses); + _feeds = new(feeds); + _assignments = new(assignments); + + _syncState.TrySetResult(); + } + + /// + /// returns the raw payload for . + /// + public async Task> GetWarId(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _warId; + } + + /// + /// returns the raw payload for . + /// + public async Task> GetWarStatus(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _statuses.Get(); + } + + /// + /// returns the raw payload for . + /// + public async Task> GetWarInfo(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _warInfo; + } + + /// + /// returns the raw payload for . + /// + public async Task> GetWarSummary(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _warSummary; + } + + /// + /// returns the raw payload for s. + /// + public async Task> GetNewsFeeds(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _feeds.Get(); + } + + /// + /// returns the raw payload for s. + /// + public async Task> GetAssignments(CancellationToken cancellationToken) + { + await _syncState.Task.WaitAsync(cancellationToken); + + return _assignments.Get(); + } +} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/AssignmentStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/AssignmentStore.cs deleted file mode 100644 index 72e4a2c..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/AssignmentStore.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Helldivers.Core.Contracts.Collections; -using Helldivers.Core.Localization; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public class AssignmentStore : StoreBase -{ - private CultureDictionary> _translations = null!; - - /// - public ValueTask SetStore(IEnumerable>> assignments) - { - _translations = new(assignments); - - return SetStore(_translations.Get()!); - } - - /// - public override async Task> AllAsync(CancellationToken cancellationToken = default) - { - await base.AllAsync(cancellationToken); - - return _translations.Get()!; - } - - /// - public override async Task GetAsync(int key, CancellationToken cancellationToken = default) - { - await base.GetAsync(key, cancellationToken); - - return _translations.Get()?.FirstOrDefault(item => GetAsyncPredicate(item, key)); - } - - /// - protected override bool GetAsyncPredicate(Assignment assignment, int key) - => assignment.Id32 == key; -} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/NewsFeedStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/NewsFeedStore.cs deleted file mode 100644 index 00a94c7..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/NewsFeedStore.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Helldivers.Core.Contracts.Collections; -using Helldivers.Core.Localization; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public class NewsFeedStore : StoreBase -{ - private CultureDictionary> _translations = null!; - - /// - public ValueTask SetStore(IEnumerable>> translations) - { - _translations = new(translations); - - return SetStore(_translations.Get()!); - } - - /// - public override async Task> AllAsync(CancellationToken cancellationToken = default) - { - await base.AllAsync(cancellationToken); - - return _translations.Get()!; - } - - /// - public override async Task GetAsync(int key, CancellationToken cancellationToken = default) - { - await base.GetAsync(key, cancellationToken); - - return _translations.Get()?.FirstOrDefault(item => GetAsyncPredicate(item, key)); - } - - /// - protected override bool GetAsyncPredicate(NewsFeedItem item, int key) - => item.Id == key; -} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/WarIdStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/WarIdStore.cs deleted file mode 100644 index ac16321..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/WarIdStore.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Helldivers.Core.Contracts; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public class WarIdStore : StoreBase -{ - // -} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/WarInfoStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/WarInfoStore.cs deleted file mode 100644 index 0947e3d..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/WarInfoStore.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Helldivers.Core.Contracts; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public sealed class WarInfoStore : StoreBase -{ - // -} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/WarStatusStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/WarStatusStore.cs deleted file mode 100644 index b0df3eb..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/WarStatusStore.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Helldivers.Core.Contracts; -using Helldivers.Core.Localization; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public sealed class WarStatusStore : StoreBase -{ - private CultureDictionary _translations = null!; - - /// - public ValueTask SetStore(IEnumerable> translations) - { - _translations = new(translations); - - return SetStore(_translations.Get()!); - } - - /// - public override async Task Get(CancellationToken cancellationToken = default) - { - await base.Get(cancellationToken); - - return _translations.Get()!; - } -} diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/WarSummaryStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/WarSummaryStore.cs deleted file mode 100644 index f424198..0000000 --- a/src/Helldivers-2-Core/Storage/ArrowHead/WarSummaryStore.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Helldivers.Core.Contracts; -using Helldivers.Models.ArrowHead; - -namespace Helldivers.Core.Storage.ArrowHead; - -/// -public class WarSummaryStore : StoreBase -{ - // -} diff --git a/src/Helldivers-2-Core/StorageFacade.cs b/src/Helldivers-2-Core/StorageFacade.cs index 41f5cf4..a68fc89 100644 --- a/src/Helldivers-2-Core/StorageFacade.cs +++ b/src/Helldivers-2-Core/StorageFacade.cs @@ -1,6 +1,9 @@ using Helldivers.Core.Facades; -using Helldivers.Models.ArrowHead; +using Helldivers.Core.Storage.ArrowHead; +using Helldivers.Models; using Helldivers.Models.Steam; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; namespace Helldivers.Core; @@ -8,7 +11,7 @@ namespace Helldivers.Core; /// Rather than having the sync service be aware of all mappings and storage versions, /// this facade class handles dispatching incoming data to the correct underlying stores. /// -public sealed class StorageFacade(ArrowHeadFacade arrowHead, SteamFacade steam, V1Facade v1) +public sealed class StorageFacade(ArrowHeadStore arrowHead, SteamFacade steam, V1Facade v1) { /// /// Updates all stores that rely on . @@ -19,9 +22,45 @@ public ValueTask UpdateStores(SteamNewsFeed feed) /// /// Updates all stores that rely on ArrowHead's models. /// - public async ValueTask UpdateStores(WarId warId, WarInfo warInfo, Dictionary warStatuses, WarSummary warSummary, Dictionary> newsFeeds, Dictionary> assignments) + public async ValueTask UpdateStores(Memory rawWarId, Memory rawWarInfo, + Dictionary> rawWarStatuses, Memory rawWarSummary, + Dictionary> rawNewsFeeds, Dictionary> rawAssignments) { - await arrowHead.UpdateStores(warId, warInfo, warStatuses, warSummary, newsFeeds, assignments); - await v1.UpdateStores(warId, warInfo, warStatuses, warSummary, newsFeeds, assignments); + arrowHead.UpdateRawStore( + rawWarId, + rawWarInfo, + rawWarSummary, + rawWarStatuses, + rawNewsFeeds, + rawAssignments + ); + + var warId = DeserializeOrThrow(rawWarId, ArrowHeadSerializerContext.Default.WarId); + var warInfo = DeserializeOrThrow(rawWarInfo, ArrowHeadSerializerContext.Default.WarInfo); + var warStatuses = rawWarStatuses.ToDictionary( + pair => pair.Key, + pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.WarStatus) + ); + var warSummary = DeserializeOrThrow(rawWarSummary, ArrowHeadSerializerContext.Default.WarSummary); + var newsFeeds = rawNewsFeeds.ToDictionary( + pair => pair.Key, + pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.ListNewsFeedItem) + ); + var assignments = rawAssignments.ToDictionary( + pair => pair.Key, + pair => DeserializeOrThrow(pair.Value, ArrowHeadSerializerContext.Default.ListAssignment) + ); + + await v1.UpdateStores( + warId, + warInfo, + warStatuses, + warSummary, + newsFeeds, + assignments + ); } + + private T DeserializeOrThrow(Memory memory, JsonTypeInfo typeInfo) + => JsonSerializer.Deserialize(memory.Span, typeInfo) ?? throw new InvalidOperationException(); } diff --git a/src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs b/src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs index 4b2b8e7..182755f 100644 --- a/src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs +++ b/src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs @@ -71,31 +71,31 @@ private async Task SynchronizeAsync(IServiceProvider services, CancellationToken { var api = services.GetRequiredService(); - var warId = await api.GetCurrentSeason(cancellationToken); + var (rawWarId, warId) = await api.GetCurrentSeason(cancellationToken); var season = warId.Id.ToString(CultureInfo.InvariantCulture); var warInfo = await api.GetWarInfo(season, cancellationToken); var warSummary = await api.GetSummary(season, cancellationToken); // For each language, load war status. - var statuses = await DownloadTranslations( + var statuses = await DownloadTranslations( language => api.GetWarStatus(season, language, cancellationToken), cancellationToken ); // For each language, load news feed. - var feeds = await DownloadTranslations( - async language => await api.LoadFeed(season, language, cancellationToken).ToListAsync(cancellationToken), + var feeds = await DownloadTranslations( + async language => await api.LoadFeed(season, language, cancellationToken), cancellationToken ); // For each language, load assignments - var assignments = await DownloadTranslations( - async language => await api.LoadAssignments(season, language, cancellationToken).ToListAsync(cancellationToken), + var assignments = await DownloadTranslations( + async language => await api.LoadAssignments(season, language, cancellationToken), cancellationToken ); await storage.UpdateStores( - warId, + rawWarId, warInfo, statuses, warSummary, @@ -104,7 +104,7 @@ await storage.UpdateStores( ); } - private async Task> DownloadTranslations(Func> func, CancellationToken cancellationToken) where T : class + private async Task>> DownloadTranslations(Func>> func, CancellationToken cancellationToken) { return await configuration.Value.Languages .ToAsyncEnumerable() @@ -114,54 +114,17 @@ private async Task> DownloadTranslations(Func(language, result); + return new KeyValuePair?>(language, result); } catch (Exception exception) { LogFailedToLoadTranslation(logger, exception, language, typeof(T).Name); - return new KeyValuePair(language, null); + return new KeyValuePair?>(language, null); } }) .SelectAwait(async task => await task) .Where(pair => pair.Value is not null) - .ToDictionaryAsync(pair => pair.Key, pair => pair.Value!, cancellationToken: cancellationToken); - } - - /// Helper function to download the war status or return null if anything fails. - private async ValueTask> AttemptToLoadWarStatus(ArrowHeadApiService arrowHeadApi, string season, - string language, CancellationToken cancellationToken) - { - try - { - var status = await arrowHeadApi.GetWarStatus(season, language, cancellationToken); - - return new(language, status); - } - catch (Exception exception) - { - LogFailedToLoadTranslation(logger, exception, language, nameof(WarStatus)); - - return new(language, null); - } - } - - /// Helper function to download a list of or return null if anything fails. - private async ValueTask?>> AttemptToLoadTranslations( - Func> api, string season, string language, - CancellationToken cancellationToken) - { - try - { - var items = await api(season, language, cancellationToken).ToListAsync(cancellationToken); - - return new(language, items); - } - catch (Exception exception) - { - LogFailedToLoadTranslation(logger, exception, language, typeof(T).Name); - - return new(language, null); - } + .ToDictionaryAsync(pair => pair.Key, pair => pair.Value.GetValueOrDefault(), cancellationToken: cancellationToken); } } diff --git a/src/Helldivers-2-Sync/Services/ArrowHeadApiService.cs b/src/Helldivers-2-Sync/Services/ArrowHeadApiService.cs index 0f7ffd9..51985c0 100644 --- a/src/Helldivers-2-Sync/Services/ArrowHeadApiService.cs +++ b/src/Helldivers-2-Sync/Services/ArrowHeadApiService.cs @@ -19,100 +19,78 @@ HttpClient http /// /// Gets the identifier of the current war season from ArrowHead's API. /// - public async Task GetCurrentSeason(CancellationToken cancellationToken) + public async Task<(Memory, WarId)> GetCurrentSeason(CancellationToken cancellationToken) { var request = BuildRequest("/api/WarSeason/current/WarID"); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - var warId = await JsonSerializer - .DeserializeAsync(stream, ArrowHeadSerializerContext.Default.WarId, cancellationToken) - ?? throw new InvalidOperationException(); + var memory = await CollectStream(stream, cancellationToken); + var warId = JsonSerializer.Deserialize( + memory.Span, + ArrowHeadSerializerContext.Default.WarId + ) ?? throw new InvalidOperationException(); - return warId; + return (memory, warId); } /// /// Fetch from ArrowHead's API. /// - public async Task GetWarInfo(string season, CancellationToken cancellationToken) + public async Task> GetWarInfo(string season, CancellationToken cancellationToken) { var request = BuildRequest($"/api/WarSeason/{season}/WarInfo"); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - return await JsonSerializer - .DeserializeAsync(stream, ArrowHeadSerializerContext.Default.WarInfo, cancellationToken) - ?? throw new InvalidOperationException(); + return await CollectStream(stream, cancellationToken); } /// /// Fetch from ArrowHead's API. /// - public async Task GetWarStatus(string season, string language, CancellationToken cancellationToken) + public async Task> GetWarStatus(string season, string language, CancellationToken cancellationToken) { var request = BuildRequest($"/api/WarSeason/{season}/Status", language); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - return await JsonSerializer - .DeserializeAsync(stream, ArrowHeadSerializerContext.Default.WarStatus, cancellationToken) - ?? throw new InvalidOperationException(); + return await CollectStream(stream, cancellationToken); } /// /// Fetch the newsfeed of a given in . /// - public async IAsyncEnumerable LoadFeed(string season, string language, - [EnumeratorCancellation] CancellationToken cancellationToken) + public async Task> LoadFeed(string season, string language, CancellationToken cancellationToken) { var request = BuildRequest($"/api/NewsFeed/{season}?maxEntries=1024", language); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - var items = JsonSerializer.DeserializeAsyncEnumerable( - stream, - ArrowHeadSerializerContext.Default.NewsFeedItem, - cancellationToken - ); - - // Unfortunately C# doesn't allow directly returning IAsyncEnumerable. - await foreach (var item in items) - yield return item ?? throw new InvalidOperationException("Failed to deserialize newsfeed item"); + return await CollectStream(stream, cancellationToken); } /// /// Loads assignments of a given in . /// - public async IAsyncEnumerable LoadAssignments(string season, string language, - [EnumeratorCancellation] CancellationToken cancellationToken) + public async Task> LoadAssignments(string season, string language, CancellationToken cancellationToken) { var request = BuildRequest($"/api/v2/Assignment/War/{season}", language); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - var assignments = JsonSerializer.DeserializeAsyncEnumerable( - stream, - ArrowHeadSerializerContext.Default.Assignment, - cancellationToken - ); - - // Unfortunately C# doesn't allow directly returning IAsyncEnumerable. - await foreach (var assignment in assignments) - yield return assignment ?? throw new InvalidOperationException("Failed to deserialize assignment"); + return await CollectStream(stream, cancellationToken); } /// /// Fetch from ArrowHead's API. /// - public async Task GetSummary(string season, CancellationToken cancellationToken) + public async Task> GetSummary(string season, CancellationToken cancellationToken) { var request = BuildRequest($"/api/Stats/war/{season}/summary"); using var response = await http.SendAsync(request, cancellationToken); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - return await JsonSerializer - .DeserializeAsync(stream, ArrowHeadSerializerContext.Default.WarSummary, cancellationToken) - ?? throw new InvalidOperationException(); + return await CollectStream(stream, cancellationToken); } private HttpRequestMessage BuildRequest(string url, string? language = null) @@ -125,4 +103,15 @@ private HttpRequestMessage BuildRequest(string url, string? language = null) Headers = { AcceptLanguage = { StringWithQualityHeaderValue.Parse(language) } } }; } + + /// + /// Read the and store it in a local buffer that can be used with zero-copy semantics. + /// + private async Task> CollectStream(Stream stream, CancellationToken cancellationToken) + { + using var memory = new MemoryStream(); + await stream.CopyToAsync(memory, cancellationToken); + + return new Memory(memory.ToArray()); + } } From 0db69732b3974516b538bbb18e0b037ddb4e7b0d Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Thu, 4 Apr 2024 02:06:52 +0200 Subject: [PATCH 2/3] make update interval 20 seconds --- src/Helldivers-2-API/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helldivers-2-API/appsettings.json b/src/Helldivers-2-API/appsettings.json index 01eaac8..1cd18d4 100644 --- a/src/Helldivers-2-API/appsettings.json +++ b/src/Helldivers-2-API/appsettings.json @@ -10,7 +10,7 @@ "AllowedHosts": "*", "Helldivers": { "Synchronization": { - "IntervalSeconds": 300, + "IntervalSeconds": 20, "DefaultLanguage": "en-US", "Languages": [ "en-US", From 85c6720b8dd3e2f267d04d017694b44eba7580c7 Mon Sep 17 00:00:00 2001 From: Wannes Gennar Date: Thu, 4 Apr 2024 02:11:52 +0200 Subject: [PATCH 3/3] fix: suppress nullability nullability is guarded by `_syncState` --- src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs b/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs index 6ef2ed3..3b8c610 100644 --- a/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs +++ b/src/Helldivers-2-Core/Storage/ArrowHead/ArrowHeadStore.cs @@ -11,9 +11,9 @@ public sealed class ArrowHeadStore private Memory _warId; private Memory _warInfo; private Memory _warSummary; - private CultureDictionary> _statuses; - private CultureDictionary> _feeds; - private CultureDictionary> _assignments; + private CultureDictionary> _statuses = null!; + private CultureDictionary> _feeds = null!; + private CultureDictionary> _assignments = null!; private readonly TaskCompletionSource _syncState = new(); ///