fix(theme): recognize migration publish echoes

Keep the submitted migration coordinate so its relay echo can complete the original acknowledgement instead of being treated as competing remote state.

Co-authored-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
Kenny Lopez
2026-08-19 12:53:11 +01:00
committed by kenny lopez
parent 2f5e99420f
commit 11fa2a53c8
3 changed files with 87 additions and 5 deletions

View File

@@ -228,6 +228,7 @@ export function CommunityThemeController() {
clearCommunityThemeMigrationOutbox(pubkey, relayUrl);
const migrationWasSubmitted = manager.cancelPendingPublish(
remote.preference,
remote,
);
cacheAndApplyCommunityTheme(
pubkey,

View File

@@ -745,6 +745,74 @@ test("remote accepted after submit is republished after the stale event settles"
}
});
test("migration self-echo during submission acknowledges without replacement", async () => {
const timer = installFakeTimer();
const publish = Promise.withResolvers();
const published = [];
const acknowledgements = [];
let liveCallback;
let signed = 0;
globalThis.window.__TAURI_INTERNALS__ = {
invoke(command, args) {
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
if (command === "nip44_decrypt_from_self") {
return Promise.resolve(JSON.stringify(preference));
}
if (command === "sign_event") {
signed += 1;
return Promise.resolve(
JSON.stringify(
relayEvent({
id: `event-${signed}`,
content: args.content,
created_at: args.createdAt,
}),
),
);
}
throw new Error(`unexpected command: ${command}`);
},
};
mock.method(relayClient, "subscribeLive", (_filter, callback) => {
liveCallback = callback;
return Promise.resolve(async () => {});
});
mock.method(relayClient, "publishEvent", (event) => {
published.push(event);
return publish.promise;
});
try {
const manager = new CommunityThemeSyncManager("alice", (event) => {
acknowledgements.push(event);
});
await manager.subscribe((remote) => {
manager.acceptRemote(remote);
manager.cancelPendingPublish(remote.preference, remote);
});
manager.publish(preference);
timer.fire();
await waitUntil(() => published.length === 1);
liveCallback(published[0]);
await new Promise((resolve) => setImmediate(resolve));
publish.resolve();
await waitUntil(() => acknowledgements.length === 1);
assert.equal(published.length, 1);
assert.equal(timer.pending(), false);
assert.deepEqual(manager.getPending(), null);
assert.deepEqual(acknowledgements[0], {
preference,
createdAt: published[0].created_at,
eventId: published[0].id,
});
} finally {
delete globalThis.window.__TAURI_INTERNALS__;
timer.restore();
mock.reset();
}
});
test("live legacy updates inherit the latest complete appearance", async () => {
const initial = { ...preference, glassOpacity: 40 };
const latest = {

View File

@@ -105,7 +105,7 @@ export class CommunityThemeSyncManager {
private readonly onPublished: (published: PublishedCommunityTheme) => void;
private readonly getLegacyFallback: () => CommunityThemePreference;
private publishGeneration = 0;
private activePublishSubmitted = false;
private activeSubmittedPublish: PublishedCommunityTheme | null = null;
private replacementAfterCancel: CommunityThemePreference | null = null;
constructor(
@@ -174,12 +174,12 @@ export class CommunityThemeSyncManager {
private startPublish(): void {
if (this.destroyed || this.publishInFlight || !this.pending) return;
this.publishInFlight = true;
this.activePublishSubmitted = false;
this.activeSubmittedPublish = null;
const preference = this.pending;
const generation = this.publishGeneration;
void this.doPublish(preference, generation).finally(() => {
this.publishInFlight = false;
this.activePublishSubmitted = false;
this.activeSubmittedPublish = null;
if (this.replacementAfterCancel) {
this.pending = this.replacementAfterCancel;
this.replacementAfterCancel = null;
@@ -222,8 +222,17 @@ export class CommunityThemeSyncManager {
cancelPendingPublish(
replacementAfterSubmit?: CommunityThemePreference,
acceptedRemote?: RemoteCommunityTheme,
): boolean {
const submitted = this.activePublishSubmitted;
if (
this.activeSubmittedPublish &&
acceptedRemote &&
acceptedRemote.createdAt === this.activeSubmittedPublish.createdAt &&
acceptedRemote.eventId === this.activeSubmittedPublish.eventId
) {
return false;
}
const submitted = this.activeSubmittedPublish !== null;
this.replacementAfterCancel =
submitted && replacementAfterSubmit ? replacementAfterSubmit : null;
this.publishGeneration += 1;
@@ -272,7 +281,11 @@ export class CommunityThemeSyncManager {
],
});
if (wasCancelled()) return;
this.activePublishSubmitted = true;
this.activeSubmittedPublish = {
preference,
createdAt: event.created_at,
eventId: event.id,
};
await relayClient.publishEvent(
event,
"Timed out publishing community theme.",