diff --git a/desktop/src/features/messages/lib/draftSync.test.mjs b/desktop/src/features/messages/lib/draftSync.test.mjs index 31e7bd8bf..dbf9de571 100644 --- a/desktop/src/features/messages/lib/draftSync.test.mjs +++ b/desktop/src/features/messages/lib/draftSync.test.mjs @@ -383,10 +383,12 @@ test("test_unuploaded_attachment_cancels_stale_text_publish", async () => { function deferred() { let resolve; - const promise = new Promise((complete) => { + let reject; + const promise = new Promise((complete, fail) => { resolve = complete; + reject = fail; }); - return { promise, resolve }; + return { promise, resolve, reject }; } test("test_newer_edit_during_inflight_publish_survives", async () => { @@ -469,3 +471,77 @@ test("test_deletion_during_inflight_publish_wins", async () => { assert.ok(rebasedTombstone); assert.equal(loadDraftEntry(channelA), undefined); }); + +test("test_stale_tombstone_completion_preserves_rebased_delete", async () => { + setup(); + const published = []; + const draftPublish = deferred(); + const staleTombstone = deferred(); + const rebasedTombstone = deferred(); + const rebasedTombstoneStarted = deferred(); + const manager = new DraftSyncManager(pubkey, "wss://relay.example", { + deriveAddress: async () => "address-a", + encrypt: async (content) => content, + fetchEvents: async () => [], + sign: async (input) => ({ + id: `signed-${published.length}`, + created_at: + input.content === "" && published.length === 1 + ? (input.createdAt ?? 0) - 1 + : (input.createdAt ?? 0), + kind: input.kind, + pubkey, + content: input.content, + sig: "", + tags: input.tags, + }), + publishEvent: async (event) => { + const publishIndex = published.push(event); + if (publishIndex === 1) await draftPublish.promise; + if (publishIndex === 2) await staleTombstone.promise; + if (publishIndex === 3) { + rebasedTombstoneStarted.resolve(); + await rebasedTombstone.promise; + } + }, + }); + + const local = draft(channelA, "draft to delete"); + saveDraftEntry(channelA, local); + manager.queuePublish(channelA, local); + const flush = manager.flushPublishes(); + while (published.length === 0) await Promise.resolve(); + const deletion = manager.queueDeletion(channelA, channelA); + removeRemoteDraftEntry(channelA); + while (published.length < 2) await Promise.resolve(); + draftPublish.resolve(); + const draftEvent = published[0]; + staleTombstone.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await rebasedTombstoneStarted.promise; + rebasedTombstone.reject(new Error("offline")); + await flush; + await deletion; + + const retried = []; + const retryManager = new DraftSyncManager(pubkey, "wss://relay.example", { + deriveAddress: async () => "address-a", + publishEvent: async (event) => retried.push(event), + sign: async (input) => ({ + id: "retry", + created_at: input.createdAt ?? 0, + kind: input.kind, + pubkey, + content: input.content, + sig: "", + tags: input.tags, + }), + }); + retryManager.start(); + await Promise.resolve(); + await retryManager.destroy(); + await manager.destroy(); + + assert.ok(retried.some((event) => event.content === "")); + assert.ok(retried.every((event) => event.created_at > draftEvent.created_at)); +}); diff --git a/desktop/src/features/messages/lib/draftSync.ts b/desktop/src/features/messages/lib/draftSync.ts index d9863aab9..6de775d49 100644 --- a/desktop/src/features/messages/lib/draftSync.ts +++ b/desktop/src/features/messages/lib/draftSync.ts @@ -327,9 +327,13 @@ export class DraftSyncManager { "Timed out deleting draft.", "Failed to delete draft.", ); - if (state.pendingDeletion?.address === pending.address) { - state.pendingDeletion = undefined; - state.remoteHead = event; + const current = this.state.get(pending.address) ?? state; + if ( + current.pendingDeletion?.address === pending.address && + (!current.remoteHead || compareHeads(event, current.remoteHead) >= 0) + ) { + current.pendingDeletion = undefined; + current.remoteHead = event; this.writeSidecar(); } } catch (error) {