mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): release remote-tombstone draft sync
A remote tombstone that already won a raced publish could leave a synthetic deletion intent durable forever and block future edits. Complete a deletion based on the winning head, and let an explicit later edit carry a known tombstone base so it can supersede that delete. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
co-authored by
Will Pfleger
parent
ca85fbf620
commit
325ff603ed
@@ -545,3 +545,56 @@ test("test_stale_tombstone_completion_preserves_rebased_delete", async () => {
|
||||
assert.ok(retried.some((event) => event.content === ""));
|
||||
assert.ok(retried.every((event) => event.created_at > draftEvent.created_at));
|
||||
});
|
||||
|
||||
test("test_remote_tombstone_during_publish_allows_later_draft", async () => {
|
||||
setup();
|
||||
const published = [];
|
||||
const draftPublish = deferred();
|
||||
const remoteTombstone = wrapped({
|
||||
id: "remote-tombstone",
|
||||
createdAt: Math.floor(Date.now() / 1_000) + 100,
|
||||
address: "address-a",
|
||||
channelId: channelA,
|
||||
content: "",
|
||||
});
|
||||
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.createdAt ?? 0,
|
||||
kind: input.kind,
|
||||
pubkey,
|
||||
content: input.content,
|
||||
sig: "",
|
||||
tags: input.tags,
|
||||
}),
|
||||
publishEvent: async (event) => {
|
||||
published.push(event);
|
||||
if (published.length === 1) await draftPublish.promise;
|
||||
},
|
||||
});
|
||||
|
||||
manager.queuePublish(channelA, draft(channelA, "raced draft"));
|
||||
const flush = manager.flushPublishes();
|
||||
while (published.length === 0) await Promise.resolve();
|
||||
await manager.mergeEvent(remoteTombstone);
|
||||
draftPublish.resolve();
|
||||
await flush;
|
||||
|
||||
assert.equal(
|
||||
localStorage.getItem(`buzz-draft-sync.v1:wss://relay.example:${pubkey}`),
|
||||
"{}",
|
||||
);
|
||||
|
||||
manager.queuePublish(channelA, draft(channelA, "new draft"));
|
||||
await manager.flushPublishes();
|
||||
await manager.destroy();
|
||||
|
||||
const newDraft = published.find((event) =>
|
||||
event.content.includes("new draft"),
|
||||
);
|
||||
assert.ok(newDraft);
|
||||
assert.ok(newDraft.created_at > remoteTombstone.created_at);
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ type PendingPublish = {
|
||||
draft: DraftState;
|
||||
channelId: string;
|
||||
address?: string;
|
||||
base?: RemoteHead;
|
||||
};
|
||||
type PendingDeletion = {
|
||||
draftKey: string;
|
||||
@@ -121,7 +122,12 @@ export class DraftSyncManager {
|
||||
return;
|
||||
}
|
||||
entry.draftKey = draftKey;
|
||||
entry.pendingPublish = { draftKey, draft, channelId: draft.channelId };
|
||||
entry.pendingPublish = {
|
||||
draftKey,
|
||||
draft,
|
||||
channelId: draft.channelId,
|
||||
base: entry.remoteHead?.content === "" ? entry.remoteHead : undefined,
|
||||
};
|
||||
this.state.set(draftKey, entry);
|
||||
// Resolve the opaque address while the draft is alive so normal delete
|
||||
// paths can durably record it before removing visible local state.
|
||||
@@ -248,7 +254,7 @@ export class DraftSyncManager {
|
||||
this.reschedulePublishes();
|
||||
return;
|
||||
}
|
||||
if (state.remoteHead?.content === "") {
|
||||
if (state.remoteHead?.content === "" && !pending.base) {
|
||||
state.pendingPublish = undefined;
|
||||
removeRemoteDraftEntry(pending.draftKey);
|
||||
this.reschedulePublishes();
|
||||
@@ -328,12 +334,16 @@ export class DraftSyncManager {
|
||||
"Failed to delete draft.",
|
||||
);
|
||||
const current = this.state.get(pending.address) ?? state;
|
||||
const winning =
|
||||
!current.remoteHead || compareHeads(event, current.remoteHead) >= 0
|
||||
? event
|
||||
: current.remoteHead;
|
||||
if (
|
||||
current.pendingDeletion?.address === pending.address &&
|
||||
(!current.remoteHead || compareHeads(event, current.remoteHead) >= 0)
|
||||
winning.content === ""
|
||||
) {
|
||||
current.pendingDeletion = undefined;
|
||||
current.remoteHead = event;
|
||||
current.remoteHead = winning;
|
||||
this.writeSidecar();
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user