fix(raw): a stale open can no longer feed the picker another device's config
This commit is contained in:
@@ -170,6 +170,11 @@
|
||||
}
|
||||
|
||||
var dto = await Svc.LoadTagForEditAsync(TagId);
|
||||
// Two opens can overlap on one modal instance (the tree's Edit command is synchronous and does
|
||||
// not disable while this resolves), so every post-await write is discarded unless this open is
|
||||
// still the displayed one. Last-completion-wins would point the picker at another device.
|
||||
if (_loadedKey != key || !Visible) { return; }
|
||||
|
||||
// RawTagEditDto is a record struct → a missing tag comes back as default (all-zero), not null.
|
||||
if (dto is not { } d || string.IsNullOrEmpty(d.TagId))
|
||||
{
|
||||
@@ -192,6 +197,8 @@
|
||||
try
|
||||
{
|
||||
var merged = await Svc.LoadMergedProbeConfigAsync(d.DeviceId);
|
||||
if (_loadedKey != key || !Visible) { return; } // superseded open → never install its device
|
||||
|
||||
if (merged is { } m)
|
||||
{
|
||||
_mergedConfigJson = m.MergedConfigJson;
|
||||
|
||||
@@ -20,14 +20,26 @@ internal sealed class FakeRawTreeService : IRawTreeService
|
||||
public Func<string, (string DriverType, string MergedConfigJson)?> OnLoadMergedProbeConfig { get; set; } =
|
||||
_ => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Optional asynchronous override of <see cref="OnLoadTagForEdit"/>. The sync delegates above
|
||||
/// complete inline, which cannot express two opens overlapping on one modal instance — set this to
|
||||
/// gate a load on a <see cref="TaskCompletionSource"/> and interleave the second open.
|
||||
/// </summary>
|
||||
public Func<string, Task<RawTagEditDto?>>? OnLoadTagForEditAsync { get; set; }
|
||||
|
||||
/// <summary>Optional asynchronous override of <see cref="OnLoadMergedProbeConfig"/>; see <see cref="OnLoadTagForEditAsync"/>.</summary>
|
||||
public Func<string, Task<(string DriverType, string MergedConfigJson)?>>? OnLoadMergedProbeConfigAsync { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<RawTagEditDto?> LoadTagForEditAsync(string tagId, CancellationToken ct = default)
|
||||
=> Task.FromResult(OnLoadTagForEdit(tagId));
|
||||
=> OnLoadTagForEditAsync is { } async ? async(tagId) : Task.FromResult(OnLoadTagForEdit(tagId));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(
|
||||
string deviceId, CancellationToken ct = default)
|
||||
=> Task.FromResult(OnLoadMergedProbeConfig(deviceId));
|
||||
=> OnLoadMergedProbeConfigAsync is { } async
|
||||
? async(deviceId)
|
||||
: Task.FromResult(OnLoadMergedProbeConfig(deviceId));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<RawNode>> LoadRootsAsync(CancellationToken ct = default)
|
||||
|
||||
@@ -33,6 +33,9 @@ public sealed class RawTagModalMergedConfigTests
|
||||
{
|
||||
private const string MergedConfig = """{"endpoint":"10.0.0.1:44818","slot":0}""";
|
||||
|
||||
/// <summary>A second device's config — the blob a superseded open must never install.</summary>
|
||||
private const string StaleConfig = """{"endpoint":"10.0.0.9:44818","slot":3}""";
|
||||
|
||||
[Fact]
|
||||
public async Task The_editor_receives_the_merged_device_config_not_a_stub()
|
||||
{
|
||||
@@ -98,6 +101,37 @@ public sealed class RawTagModalMergedConfigTests
|
||||
firstAccessor().ShouldBe(MergedConfig);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_stale_open_never_feeds_the_picker_another_devices_config()
|
||||
{
|
||||
// RawTree's OnEditTag is synchronous and does not disable while the modal resolves, so a rapid
|
||||
// double-click across two tag rows overlaps two opens on ONE modal instance. Every write after an
|
||||
// await must therefore be discarded if the open it belongs to is no longer the displayed one:
|
||||
// last-completion-wins would silently point a live browse at the wrong controller.
|
||||
var gate = new TaskCompletionSource<(string DriverType, string MergedConfigJson)?>();
|
||||
var svc = new FakeRawTreeService
|
||||
{
|
||||
OnLoadTagForEdit = tagId => TagOn(tagId, tagId == "tag-1" ? "dev-A" : "dev-B"),
|
||||
OnLoadMergedProbeConfigAsync = deviceId => deviceId == "dev-A"
|
||||
? gate.Task
|
||||
: Task.FromResult<(string DriverType, string MergedConfigJson)?>(("AbCip", MergedConfig)),
|
||||
};
|
||||
|
||||
var modal = NewModal(svc, "tag-1");
|
||||
var firstOpen = InvokeParametersSetAsync(modal);
|
||||
|
||||
// The second open supersedes the first while the first is still parked on dev-A's resolve.
|
||||
modal.TagId = "tag-2";
|
||||
await InvokeParametersSetAsync(modal);
|
||||
|
||||
gate.SetResult(("AbCip", StaleConfig));
|
||||
await firstOpen;
|
||||
|
||||
DriverConfigJson(modal).ShouldBe(
|
||||
MergedConfig,
|
||||
"the superseded open's dev-A config overwrote the displayed tag's dev-B config");
|
||||
}
|
||||
|
||||
private static RawTagEditDto TagOn(string tagId, string deviceId) => new(
|
||||
tagId,
|
||||
deviceId,
|
||||
|
||||
Reference in New Issue
Block a user