From 97d2d195c30a70c9888a9c01624756245931fe53 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Mon, 19 Oct 2026 14:49:40 -0400 Subject: [PATCH] fix(outbox): two tabs no longer overwrite each other's queue (#268) OutboxFeature loaded lotus_outbox_v1 once and then saved its own in-memory list over the key on every change, so a second tab's save dropped the first tab's unsent messages. persist() now re-reads the stored list and applies the change (add/remove by txnId) to it. Fixes #268 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/app/features/outbox/OutboxFeature.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/features/outbox/OutboxFeature.tsx b/src/app/features/outbox/OutboxFeature.tsx index c8c0d3a89..b80586ba4 100644 --- a/src/app/features/outbox/OutboxFeature.tsx +++ b/src/app/features/outbox/OutboxFeature.tsx @@ -94,10 +94,15 @@ export function OutboxFeature() { OFFLINE.has(mx.getSyncState()) || (typeof navigator !== 'undefined' && navigator.onLine === false); - const persist = (next: OutboxEntry[]) => { - if (next === entries) return; + // [Gitea #268] Read-modify-write: another tab may have queued or cleared + // entries since we last looked, and saving our own copy over the key + // would drop theirs. Entries are added/removed by txnId, so applying our + // change to the stored list merges cleanly. + const persist = (update: (list: OutboxEntry[]) => OutboxEntry[]) => { + const stored = loadOutbox(userId); + const next = update(stored); entries = next; - saveOutbox(userId, entries); + if (next !== stored) saveOutbox(userId, next); }; /** @@ -123,8 +128,8 @@ export function OutboxFeature() { if (!shouldKeepInOutbox(event.getType(), event.getContent())) return; live.set(txnId, { event, room }); if (event.status === EventStatus.NOT_SENT) scheduleBlipRetry(event); - persist( - withEntry(entries, { + persist((list) => + withEntry(list, { txnId, roomId: room.roomId, threadId: event.threadRootId ?? null, @@ -138,7 +143,7 @@ export function OutboxFeature() { // SENT, CANCELLED, or the remote echo replaced it (status null). live.delete(txnId); autoRetries.delete(txnId); - persist(withoutEntry(entries, txnId)); + persist((list) => withoutEntry(list, txnId)); }; /** Re-send network failures, oldest first; a room stops at its first failure. */ @@ -191,7 +196,7 @@ export function OutboxFeature() { return !!room && isDelivered(room, entry.txnId); }, ); - plan.drop.forEach((entry) => persist(withoutEntry(entries, entry.txnId))); + plan.drop.forEach((entry) => persist((list) => withoutEntry(list, entry.txnId))); plan.restore.forEach((entry) => { const room = mx.getRoom(entry.roomId); if (!room || live.has(entry.txnId)) return;