Skip to content

Commit 33d9aa2

Browse files
authored
Merge pull request #3199 from Nexus-Mods/replace-mod-in-place-v2
Added: Replace Library Items Atomically in Place & Other Library Refactorings
2 parents 4d5f8b5 + f8ac9de commit 33d9aa2

File tree

20 files changed

+565
-111
lines changed

20 files changed

+565
-111
lines changed

src/NexusMods.Abstractions.Jobs/IJobMonitor.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ public interface IJobMonitor
1212
/// Starts a job given the job definition and the code to run as part of the job.
1313
/// </summary>
1414
IJobTask<TJobType, TResultType> Begin<TJobType, TResultType>(TJobType job, Func<IJobContext<TJobType>, ValueTask<TResultType>> task)
15-
where TJobType : IJobDefinition<TResultType>
15+
where TJobType : IJobDefinition<TResultType>
1616
where TResultType : notnull;
17-
18-
17+
18+
1919
/// <summary>
2020
/// Starts a job given the job definition.
2121
/// </summary>
2222
IJobTask<TJobType, TResultType> Begin<TJobType, TResultType>(TJobType job)
23-
where TJobType : IJobDefinitionWithStart<TJobType, TResultType>
23+
where TJobType : IJobDefinitionWithStart<TJobType, TResultType>
2424
where TResultType : notnull;
2525

2626
/// <summary>

src/NexusMods.Abstractions.Library/ILibraryService.cs

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
namespace NexusMods.Abstractions.Library;
1414

1515
/// <summary>
16-
/// Represents the library.
16+
/// Represents the library, this class provides access to various functionalities
17+
/// that are accessible from within a 'library' related view.
1718
/// </summary>
1819
[PublicAPI]
1920
public interface ILibraryService
@@ -28,30 +29,171 @@ public interface ILibraryService
2829
/// </summary>
2930
IJobTask<IAddLocalFile, LocalFile.ReadOnly> AddLocalFile(AbsolutePath absolutePath);
3031

32+
/// <summary>
33+
/// Returns all loadouts that contain the given library item.
34+
/// </summary>
35+
/// <param name="libraryItem">The item to search for.</param>
36+
/// <remarks>
37+
/// The loadout and linked item to the current item.
38+
/// </remarks>
39+
IEnumerable<(Loadout.ReadOnly loadout, LibraryLinkedLoadoutItem.ReadOnly linkedItem)> LoadoutsWithLibraryItem(LibraryItem.ReadOnly libraryItem);
40+
3141
/// <summary>
3242
/// Adds a library file.
3343
/// </summary>
3444
Task<LibraryFile.New> AddLibraryFile(ITransaction transaction, AbsolutePath source);
3545

3646
/// <summary>
3747
/// Installs a library item into a target loadout.
48+
/// To remove an installed item, use <see cref="RemoveLinkedItemFromLoadout"/>.
3849
/// </summary>
3950
/// <param name="libraryItem">The item to install.</param>
4051
/// <param name="targetLoadout">The target loadout.</param>
4152
/// <param name="parent">If specified the installed item will be placed in this group, otherwise it will default to the user's local collection</param>
4253
/// <param name="installer">The Library will use this installer to install the item</param>
43-
/// <param name="fallbackInstaller">Fallback installer instead of the default advanced installer</param>
44-
IJobTask<IInstallLoadoutItemJob, LoadoutItemGroup.ReadOnly> InstallItem(
54+
/// <param name="fallbackInstaller">The installer to use if the default installer fails</param>
55+
/// <param name="transaction">The transaction to attach the installation to. Install is only completed when transaction is completed.</param>
56+
/// <remarks>
57+
/// Job returns a result with null <see cref="LoadoutItemGroup.ReadOnly"/> after
58+
/// if supplied an external transaction via <paramref name="transaction"/>,
59+
/// since it is the caller's responsibility to complete that transaction.
60+
/// </remarks>
61+
IJobTask<IInstallLoadoutItemJob, InstallLoadoutItemJobResult> InstallItem(
4562
LibraryItem.ReadOnly libraryItem,
4663
LoadoutId targetLoadout,
4764
Optional<LoadoutItemGroupId> parent = default,
4865
ILibraryItemInstaller? installer = null,
49-
ILibraryItemInstaller? fallbackInstaller = null);
66+
ILibraryItemInstaller? fallbackInstaller = null,
67+
ITransaction? transaction = null);
5068

5169
/// <summary>
5270
/// Removes a number of items from the library.
71+
/// This will automatically unlink the loadouts from the items are part of.
5372
/// </summary>
5473
/// <param name="libraryItems">The items to remove from the library.</param>
5574
/// <param name="gcRunMode">Defines how the garbage collector should be run</param>
56-
Task RemoveItems(IEnumerable<LibraryItem.ReadOnly> libraryItems, GarbageCollectorRunMode gcRunMode = GarbageCollectorRunMode.RunAsynchronously);
75+
Task RemoveLibraryItems(IEnumerable<LibraryItem.ReadOnly> libraryItems, GarbageCollectorRunMode gcRunMode = GarbageCollectorRunMode.RunAsynchronously);
76+
77+
/// <summary>
78+
/// Removes a single linked loadout item from its loadout,
79+
/// managing the transaction automatically.
80+
/// </summary>
81+
/// <param name="itemId">The ID of the linked loadout item to remove from the loadout.</param>
82+
Task RemoveLinkedItemFromLoadout(LibraryLinkedLoadoutItemId itemId);
83+
84+
/// <summary>
85+
/// Removes multiple linked loadout items from their loadout,
86+
/// managing the transaction automatically.
87+
/// </summary>
88+
/// <param name="itemIds">The IDs of the linked loadout items to remove from their loadout.</param>
89+
Task RemoveLinkedItemsFromLoadout(IEnumerable<LibraryLinkedLoadoutItemId> itemIds);
90+
91+
/// <summary>
92+
/// Removes a single linked loadout item from a loadout,
93+
/// using the provided transaction.
94+
/// </summary>
95+
/// <param name="itemId">The ID of the linked loadout item to remove from its loadout.</param>
96+
/// <param name="tx">Existing transaction to use for this operation.</param>
97+
void RemoveLinkedItemFromLoadout(LibraryLinkedLoadoutItemId itemId, ITransaction tx);
98+
99+
/// <summary>
100+
/// Removes multiple linked loadout items from their loadout,
101+
/// using the provided transaction.
102+
/// </summary>
103+
/// <param name="itemIds">The IDs of the linked loadout items to remove from their loadout.</param>
104+
/// <param name="tx">Existing transaction to use for this operation.</param>
105+
void RemoveLinkedItemsFromLoadout(IEnumerable<LibraryLinkedLoadoutItemId> itemIds, ITransaction tx);
106+
107+
/// <summary>
108+
/// Removes all linked loadout items from all loadouts,
109+
/// using the provided transaction.
110+
/// </summary>
111+
/// <param name="libraryItems">The library items whose associated linked loadout items should be removed.</param>
112+
/// <param name="tx">Existing transaction to use for this operation.</param>
113+
void RemoveLinkedItemsFromAllLoadouts(IEnumerable<LibraryItem.ReadOnly> libraryItems, ITransaction tx);
114+
115+
/// <summary>
116+
/// Removes all linked loadout items from all loadouts,
117+
/// managing the transaction automatically.
118+
/// </summary>
119+
/// <param name="libraryItems">The library items whose associated linked loadout items should be removed.</param>
120+
Task RemoveLinkedItemsFromAllLoadouts(IEnumerable<LibraryItem.ReadOnly> libraryItems);
121+
122+
/// <summary>
123+
/// Replaces linked loadout items across all loadouts with installations of a different library item.
124+
/// </summary>
125+
/// <param name="oldItem">The library item whose linked loadout items should be replaced.</param>
126+
/// <param name="newItem">The replacement library item from which to install the new linked loadout items from.</param>
127+
/// <param name="options">Options controlling how to replace the linked loadout items.</param>
128+
/// <param name="tx">The transaction to use for this operation.</param>
129+
/// <returns>
130+
/// A result indicating success or failure of the replacement operation.
131+
/// </returns>
132+
ValueTask<LibraryItemReplacementResult> ReplaceLinkedItemsInAllLoadouts(LibraryItem.ReadOnly oldItem, LibraryItem.ReadOnly newItem, ReplaceLibraryItemOptions options, ITransaction tx);
133+
134+
/// <summary>
135+
/// Replaces multiple sets of linked loadout items across all loadouts with new versions.
136+
/// </summary>
137+
/// <param name="replacements">The pairs of library items (old and new) whose linked loadout items should be replaced.</param>
138+
/// <param name="options">Options controlling how to replace the linked loadout items.</param>
139+
/// <param name="tx">The transaction to use for this operation.</param>
140+
/// <returns>
141+
/// A result indicating success or failure of the replacement operation.
142+
/// </returns>
143+
ValueTask<LibraryItemReplacementResult> ReplaceLinkedItemsInAllLoadouts(IEnumerable<(LibraryItem.ReadOnly oldItem, LibraryItem.ReadOnly newItem)> replacements, ReplaceLibraryItemsOptions options, ITransaction tx);
144+
145+
/// <summary>
146+
/// Replaces multiple sets of linked loadout items across all loadouts with new versions,
147+
/// managing the transaction automatically.
148+
/// </summary>
149+
/// <param name="replacements">The pairs of library items (old and new) whose linked loadout items should be replaced.</param>
150+
/// <param name="options">Options controlling how to replace the linked loadout items.</param>
151+
/// <returns>
152+
/// A result indicating success or failure of the replacement operation.
153+
/// </returns>
154+
ValueTask<LibraryItemReplacementResult> ReplaceLinkedItemsInAllLoadouts(IEnumerable<(LibraryItem.ReadOnly oldItem, LibraryItem.ReadOnly newItem)> replacements, ReplaceLibraryItemsOptions options);
155+
}
156+
157+
/// <summary>
158+
/// Represents the result of a <see cref="ILibraryService.ReplaceLinkedItemsInAllLoadouts(NexusMods.Abstractions.Library.Models.LibraryItem.ReadOnly,NexusMods.Abstractions.Library.Models.LibraryItem.ReadOnly,NexusMods.Abstractions.Library.ReplaceLibraryItemOptions,NexusMods.MnemonicDB.Abstractions.ITransaction)"/> operation.
159+
/// </summary>
160+
public enum LibraryItemReplacementResult
161+
{
162+
/// <summary>
163+
/// The operation was successful.
164+
/// </summary>
165+
Success,
166+
167+
/// <summary>
168+
/// The operation failed (unknown reason).
169+
/// </summary>
170+
FailedUnknownReason,
171+
}
172+
173+
/// <summary>
174+
/// Options for the <see cref="ILibraryService.ReplaceLinkedItemsInAllLoadouts(NexusMods.Abstractions.Library.Models.LibraryItem.ReadOnly,NexusMods.Abstractions.Library.Models.LibraryItem.ReadOnly,NexusMods.Abstractions.Library.ReplaceLibraryItemOptions,NexusMods.MnemonicDB.Abstractions.ITransaction)"/>
175+
/// API.
176+
/// </summary>
177+
public struct ReplaceLibraryItemOptions
178+
{
179+
/// <summary>
180+
/// Skips items in ReadOnly collections such as collections from Nexus Mods.
181+
/// </summary>
182+
public bool IgnoreReadOnlyCollections { get; set; }
183+
}
184+
185+
/// <summary>
186+
/// Options controlling how multiple sets of linked loadout items are replaced across loadouts.
187+
/// </summary>
188+
public struct ReplaceLibraryItemsOptions
189+
{
190+
/// <summary>
191+
/// Skips items in ReadOnly collections such as collections from Nexus Mods.
192+
/// </summary>
193+
public bool IgnoreReadOnlyCollections { get; set; }
194+
195+
/// <summary>
196+
/// Gets the <see cref="ReplaceLibraryItemOptions"/> for this <see cref="ReplaceLibraryItemsOptions"/>
197+
/// </summary>
198+
public ReplaceLibraryItemOptions ToReplaceLibraryItemOptions() => new() { IgnoreReadOnlyCollections = IgnoreReadOnlyCollections };
57199
}

src/NexusMods.Abstractions.Library/Jobs/IInstallLoadoutItemJob.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
using NexusMods.Abstractions.Library.Installers;
33
using NexusMods.Abstractions.Library.Models;
44
using NexusMods.Abstractions.Loadouts;
5+
using NexusMods.MnemonicDB.Abstractions;
56

67
namespace NexusMods.Abstractions.Library.Jobs;
78

89
/// <summary>
910
/// A job that installs a library item to a loadout
1011
/// </summary>
11-
public interface IInstallLoadoutItemJob : IJobDefinition<LoadoutItemGroup.ReadOnly>
12+
public interface IInstallLoadoutItemJob : IJobDefinition<InstallLoadoutItemJobResult>
1213
{
1314
/// <summary>
1415
/// The library item to install
@@ -30,3 +31,15 @@ public interface IInstallLoadoutItemJob : IJobDefinition<LoadoutItemGroup.ReadOn
3031
/// </summary>
3132
public ILibraryItemInstaller? Installer { get; }
3233
}
34+
35+
/// <summary>
36+
/// The result of installing a loadout item via the <see cref="IInstallLoadoutItemJob"/>.
37+
/// This struct holds a <see cref="LoadoutItemGroup"/> for the item which was just installed.
38+
///
39+
/// If the value is 'null' then the job was attached to an existing, external transaction
40+
/// to be part of a larger atomic operation.
41+
/// (Done by passing an <see cref="ITransaction"/> transaction to the job.)
42+
/// This is because the value is not yet available; as the transaction
43+
/// needs to be externally committed by the caller.
44+
/// </summary>
45+
public record struct InstallLoadoutItemJobResult(LoadoutItemGroup.ReadOnly? LoadoutItemGroup);

src/NexusMods.Abstractions.Loadouts/Models/LibraryLinkedLoadoutItem.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,13 @@ public partial class LibraryLinkedLoadoutItem : IModelDefinition
2121
/// The linked library item.
2222
/// </summary>
2323
public static readonly ReferenceAttribute<LibraryItem> LibraryItem = new(Namespace, nameof(LibraryItem)) { IsIndexed = true };
24+
25+
public readonly partial struct ReadOnly
26+
{
27+
/// <summary>
28+
/// Tries converting this entity to a <see cref="LoadoutItem"/> entity,
29+
/// if the entity is not a <see cref="LoadoutItem"/> entity, it returns false.
30+
/// </summary>
31+
public LoadoutItem.ReadOnly AsLoadoutItem() => new(Db, Id);
32+
}
2433
}
Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,31 @@
1-
using Microsoft.Extensions.DependencyInjection;
21
using NexusMods.Abstractions.Library;
32
using NexusMods.Abstractions.Library.Models;
4-
using NexusMods.Abstractions.Loadouts;
53
using NexusMods.App.UI.Overlays;
64
using NexusMods.App.UI.Overlays.LibraryDeleteConfirmation;
75
using NexusMods.MnemonicDB.Abstractions;
8-
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
9-
using NexusMods.MnemonicDB.Abstractions.TxFunctions;
106
namespace NexusMods.App.UI.Pages.Library;
117

128
/// <summary>
139
/// Utility helper class for removing a set of library items from inside a ViewModel.
1410
/// </summary>
1511
/// <remarks>
16-
/// This is here to help easier migration to new LoadoutItems based library UI
17-
/// when the time comes.
12+
/// This wraps the actual removal from loadouts and user facing UI confirmation
13+
/// into a single operation, so you can call this from anywhere and not have to
14+
/// worry too much about it.
1815
/// </remarks>
1916
public static class LibraryItemRemover
2017
{
2118
public static async Task RemoveAsync(
2219
IConnection conn,
2320
IOverlayController overlayController,
2421
ILibraryService libraryService,
25-
LibraryItem.ReadOnly[] toRemove,
26-
CancellationToken cancellationToken = default)
22+
LibraryItem.ReadOnly[] toRemove)
2723
{
2824
var warnings = LibraryItemDeleteWarningDetector.Process(conn, toRemove);
2925
var alphaWarningViewModel = LibraryItemDeleteConfirmationViewModel.FromWarningDetector(warnings);
3026
alphaWarningViewModel.Controller = overlayController;
3127
var result = await overlayController.EnqueueAndWait(alphaWarningViewModel);
32-
if (!result) return;
33-
3428
if (result)
35-
{
36-
// Note(sewer) Can the person reviewing this code let me know their opinion of
37-
// whether this should be inlined into LibraryService or not?
38-
var loadouts = Loadout.All(conn.Db).ToArray();
39-
using var tx = conn.BeginTransaction();
40-
41-
// Note. A loadout may technically still be updated in the background via the CLI,
42-
// However this is unlikelu, and the possibility of a concurrent update
43-
// is always possible, as long as we show a blocking dialog to the user.
44-
foreach (var itemInLoadout in warnings.ItemsInLoadouts)
45-
{
46-
foreach (var loadout in loadouts)
47-
{
48-
foreach (var loadoutItem in loadout.GetLoadoutItemsByLibraryItem(itemInLoadout))
49-
tx.Delete(loadoutItem, recursive: true);
50-
}
51-
}
52-
53-
await tx.Commit();
54-
await libraryService.RemoveItems(toRemove);
55-
}
29+
await libraryService.RemoveLibraryItems(toRemove);
5630
}
5731
}

src/NexusMods.App.UI/Pages/LibraryPage/LibraryViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ private async ValueTask RemoveSelectedItems(CancellationToken cancellationToken)
315315
{
316316
var db = _connection.Db;
317317
var toRemove = GetSelectedIds().Select(id => LibraryItem.Load(db, id)).ToArray();
318-
await LibraryItemRemover.RemoveAsync(_connection, _serviceProvider.GetRequiredService<IOverlayController>(), _libraryService, toRemove, cancellationToken);
318+
await LibraryItemRemover.RemoveAsync(_connection, _serviceProvider.GetRequiredService<IOverlayController>(), _libraryService, toRemove);
319319
}
320320

321321
private async ValueTask AddFilesFromDisk(IStorageProvider storageProvider, CancellationToken cancellationToken)

src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.DependencyInjection;
88
using NexusMods.Abstractions.Collections;
99
using NexusMods.Abstractions.Games;
10+
using NexusMods.Abstractions.Library;
1011
using NexusMods.Abstractions.Loadouts;
1112
using NexusMods.Abstractions.Loadouts.Extensions;
1213
using NexusMods.App.UI.Controls;
@@ -20,7 +21,6 @@
2021
using NexusMods.Icons;
2122
using NexusMods.MnemonicDB.Abstractions;
2223
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
23-
using NexusMods.MnemonicDB.Abstractions.TxFunctions;
2424
using ObservableCollections;
2525
using OneOf;
2626
using R3;
@@ -39,6 +39,7 @@ public class LoadoutViewModel : APageViewModel<ILoadoutViewModel>, ILoadoutViewM
3939
public ReactiveCommand<Unit> CollectionToggleCommand { get; }
4040

4141
public LoadoutTreeDataGridAdapter Adapter { get; }
42+
public ILibraryService _LibraryService;
4243

4344
[Reactive] public bool IsCollection { get; private set; }
4445
[Reactive] public bool IsCollectionEnabled { get; private set; }
@@ -65,6 +66,7 @@ public LoadoutViewModel(
6566

6667
Adapter = new LoadoutTreeDataGridAdapter(serviceProvider, loadoutFilter);
6768

69+
_LibraryService = serviceProvider.GetRequiredService<ILibraryService>();
6870
var connection = serviceProvider.GetRequiredService<IConnection>();
6971

7072
if (collectionGroupId.HasValue)
@@ -177,17 +179,11 @@ public LoadoutViewModel(
177179
.SelectMany(static itemModel => GetLoadoutItemIds(itemModel))
178180
.ToHashSet()
179181
.Where(id => !IsRequired(id, connection))
182+
.Select(x => (LibraryLinkedLoadoutItemId)x.Value)
180183
.ToArray();
181184

182185
if (ids.Length == 0) return;
183-
using var tx = connection.BeginTransaction();
184-
185-
foreach (var id in ids)
186-
{
187-
tx.Delete(id, recursive: true);
188-
}
189-
190-
await tx.Commit();
186+
await _LibraryService.RemoveLinkedItemsFromLoadout(ids);
191187
},
192188
awaitOperation: AwaitOperation.Sequential,
193189
initialCanExecute: false,

src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ private async Task RemoveGame(GameInstallation installation, bool shouldDeleteDo
236236
await installation.GetGame().Synchronizer.UnManage(installation);
237237

238238
if (!shouldDeleteDownloads) return;
239-
await _libraryService.RemoveItems(filesToDelete.Select(file => file.AsLibraryItem()));
239+
await _libraryService.RemoveLibraryItems(filesToDelete.Select(file => file.AsLibraryItem()));
240240

241241
foreach (var collection in collections)
242242
{

0 commit comments

Comments
 (0)