mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-12 19:44:14 -04:00
fix(data): retry absent pending uploads without changing identity
This commit is contained in:
1 parent
94ed8a0f8f
commit
fae37dd1d0
5 files changed
+78
-9
No files matched your search
@@ -12,9 +12,11 @@ public sealed class NestSaveSession
|
||||
public int Revision { get; private set; }
|
||||
public bool RequiresReload { get; private set; }
|
||||
public Guid PendingCreateId { get; private set; }
|
||||
public bool PendingCreateWasCopy => PendingCreateId != Guid.Empty && pendingCreateWasCopy;
|
||||
private string pendingCreateUrl = "";
|
||||
private byte[]? pendingCreateArchive;
|
||||
private NestRecord? pendingCreateMetadata;
|
||||
private bool pendingCreateWasCopy;
|
||||
|
||||
public void Bind(Guid id, string serverUrl, int revision)
|
||||
{
|
||||
@@ -34,6 +36,7 @@ public sealed class NestSaveSession
|
||||
PendingCreateId = Guid.Empty;
|
||||
pendingCreateArchive = null;
|
||||
pendingCreateMetadata = null;
|
||||
pendingCreateWasCopy = false;
|
||||
pendingCreateUrl = "";
|
||||
}
|
||||
|
||||
@@ -72,10 +75,11 @@ public sealed class NestSaveSession
|
||||
if (normalizedUrl.Length == 0)
|
||||
throw new ArgumentException("A database server URL is required.", nameof(serverUrl));
|
||||
|
||||
if (PendingCreateId != Guid.Empty && (RequiresReload || normalizedUrl != pendingCreateUrl || saveCopy))
|
||||
throw new IOException("The previous upload may have succeeded. Reconcile its pending ID on the original server before saving or copying.");
|
||||
if (PendingCreateId != Guid.Empty && (RequiresReload || normalizedUrl != pendingCreateUrl
|
||||
|| saveCopy != pendingCreateWasCopy))
|
||||
throw new IOException("Reconcile the pending upload on its original server, then retry with the same Save or Save As action.");
|
||||
var id = !saveCopy && ServerUrl == normalizedUrl ? RemoteId : Guid.Empty;
|
||||
var creating = id == Guid.Empty;
|
||||
var creating = PendingCreateId != Guid.Empty || id == Guid.Empty;
|
||||
var uploadId = creating ? (PendingCreateId == Guid.Empty ? Guid.NewGuid() : PendingCreateId) : id;
|
||||
var record = NestRecordFactory.FromNest(nest, uploadId, archive.LongLength);
|
||||
if (!creating && (Revision < 1 || RequiresReload))
|
||||
@@ -86,6 +90,7 @@ public sealed class NestSaveSession
|
||||
pendingCreateUrl = normalizedUrl;
|
||||
pendingCreateArchive = archive.ToArray();
|
||||
pendingCreateMetadata = record;
|
||||
pendingCreateWasCopy = saveCopy;
|
||||
}
|
||||
NestRecord saved;
|
||||
try
|
||||
|
||||
@@ -66,6 +66,68 @@ public sealed class NestHistoryClientTests
|
||||
Assert.Single(await repository.ListAsync());
|
||||
}
|
||||
|
||||
private sealed class FirstPostFailsBeforeSend : DelegatingHandler
|
||||
{
|
||||
private bool failed;
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
{
|
||||
if (request.Method == HttpMethod.Post && !failed)
|
||||
{
|
||||
failed = true;
|
||||
throw new IOException("Upload was not sent.");
|
||||
}
|
||||
return base.SendAsync(request, ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbsentPendingUpload_RetriesSameIdOnlyAfterExplicitReconciliation()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var http = factory.CreateDefaultClient(new FirstPostFailsBeforeSend());
|
||||
using var repository = new RemoteNestRepository(http, http.BaseAddress!.ToString());
|
||||
var session = new NestSaveSession();
|
||||
var nest = NestApiTests.SyntheticNest();
|
||||
var archive = NestApiTests.Archive(nest);
|
||||
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, http.BaseAddress.ToString(), nest, archive));
|
||||
var pending = session.PendingCreateId;
|
||||
Assert.Empty(await repository.ListAsync());
|
||||
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, http.BaseAddress.ToString(), nest, archive));
|
||||
Assert.Null(await session.ReconcilePendingCreateAsync(repository, http.BaseAddress.ToString()));
|
||||
Assert.False(session.RequiresReload);
|
||||
var created = await session.SaveAsync(repository, http.BaseAddress.ToString(), nest, archive);
|
||||
Assert.Equal(pending, created.Id);
|
||||
Assert.Equal(1, created.Revision);
|
||||
Assert.Single(await repository.ListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbsentPendingSaveAs_NeverUpdatesBoundOriginalOnRetry()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var seedHttp = factory.CreateClient();
|
||||
using var seed = new RemoteNestRepository(seedHttp, seedHttp.BaseAddress!.ToString());
|
||||
var nest = NestApiTests.SyntheticNest();
|
||||
var originalBytes = NestApiTests.Archive(nest);
|
||||
var original = await seed.UploadAsync(originalBytes, new NestRecord { Name = "Original" });
|
||||
using var http = factory.CreateDefaultClient(new FirstPostFailsBeforeSend());
|
||||
using var repository = new RemoteNestRepository(http, http.BaseAddress!.ToString());
|
||||
var session = new NestSaveSession();
|
||||
session.Bind(original.Id, http.BaseAddress.ToString(), original.Revision);
|
||||
nest.Name = "Copied nest";
|
||||
var copyBytes = NestApiTests.Archive(nest);
|
||||
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, http.BaseAddress.ToString(), nest, copyBytes, saveCopy: true));
|
||||
var pending = session.PendingCreateId;
|
||||
Assert.Null(await session.ReconcilePendingCreateAsync(repository, http.BaseAddress.ToString()));
|
||||
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, http.BaseAddress.ToString(), nest, copyBytes));
|
||||
var copy = await session.SaveAsync(repository, http.BaseAddress.ToString(), nest, copyBytes, saveCopy: true);
|
||||
Assert.Equal(pending, copy.Id);
|
||||
Assert.NotEqual(original.Id, copy.Id);
|
||||
Assert.Equal(1, (await repository.GetMetadataAsync(original.Id))!.Revision);
|
||||
Assert.Equal(originalBytes, await repository.GetFileAsync(original.Id));
|
||||
Assert.Equal(copyBytes, await repository.GetFileAsync(copy.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RealClient_CreatesAndUpdatesOneNestWithExactHistoricalArchives()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace OpenNest
|
||||
|
||||
public Guid RemoteId => remoteSession.RemoteId;
|
||||
public Guid PendingCreateId => remoteSession.PendingCreateId;
|
||||
public bool PendingCreateRequiresReconciliation => remoteSession.PendingCreateId != Guid.Empty && remoteSession.RequiresReload;
|
||||
public bool PendingCreateWasCopy => remoteSession.PendingCreateWasCopy;
|
||||
public void BindRemote(Guid id, string serverUrl) => remoteSession.Bind(id, serverUrl);
|
||||
|
||||
public void BindRemote(Guid id, string serverUrl, int revision) => remoteSession.Bind(id, serverUrl, revision);
|
||||
|
||||
@@ -722,12 +722,12 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var serverUrl = NestStorage.Settings.ServerUrl;
|
||||
var repository = NestStorage.CreateRepository();
|
||||
if (form.Document.PendingCreateId != Guid.Empty)
|
||||
if (form.Document.PendingCreateRequiresReconciliation)
|
||||
{
|
||||
var reconciled = await form.Document.ReconcilePendingCreateAsync(repository, serverUrl);
|
||||
if (!IsDisposed)
|
||||
statusLabel1.Text = reconciled is null
|
||||
? "No pending nest was found. Review your work, then Save again to retry the same ID."
|
||||
? $"No pending nest was found. Review your work, then {(form.Document.PendingCreateWasCopy ? "Save As" : "Save")} again to retry the same ID."
|
||||
: $"Found saved version {reconciled.Revision}. Review the current nest before saving again.";
|
||||
return; // Never combine reconciliation with a write in the same click.
|
||||
}
|
||||
|
||||
@@ -151,10 +151,10 @@ For an ambiguous first upload (including Save As or a server switch), OpenNest
|
||||
keeps the client-generated pending ID and blocks another upload. The next Save
|
||||
checks that exact ID on the original server without writing: if it exists with
|
||||
the exact attempted archive and metadata, the document binds to its revision;
|
||||
if it is absent, a further explicit Save may retry with the same ID. If it
|
||||
differs, reopen the server record to review it; do not discard the pending
|
||||
identity or blindly create another record. Older servers remain read-only for
|
||||
existing nests until upgraded.
|
||||
if it is absent, another explicit Save (or Save As for a pending copy) may
|
||||
retry with the same ID. If it differs, reopen the server record to review it;
|
||||
do not discard the pending identity or blindly create another record. Older
|
||||
servers remain read-only for existing nests until upgraded.
|
||||
|
||||
## Server integration tests
|
||||
|
||||
|
||||
Reference in new issue
Block a user