Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/main/ipc/reticulum-db-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,38 @@ describe('reticulum destination / activity prune IPC', () => {
expect(row.delivery_method).toBe('paper');
});

it('saveReticulumMessage drops reticulum-pending twins while still sending', () => {
const identityId = 'id-rt-pending-orphan';
const senderId = 'cc'.repeat(16);
const payload = 'hello aibot';
const ts = 1_700_000_000_000;
const save = handlers.get('db:saveReticulumMessage');
save?.(event, {
identity_id: identityId,
sender_id: senderId,
sender_name: 'Me',
payload,
timestamp: ts,
message_hash: 'reticulum-pending-1700000000000',
delivery_status: 'sending',
});
save?.(event, {
identity_id: identityId,
sender_id: senderId,
sender_name: 'Me',
payload,
timestamp: ts + 182,
message_hash: 'ab'.repeat(32),
delivery_status: 'sending',
});
const rows = db!
.prepareOnce(
'SELECT message_hash, delivery_status FROM reticulum_messages WHERE identity_id = ? ORDER BY id',
)
.all(identityId) as { message_hash: string; delivery_status: string }[];
expect(rows).toEqual([{ message_hash: 'ab'.repeat(32), delivery_status: 'sending' }]);
});

it('pruneReticulumIdentityActivityByAge deletes stale millisecond last_seen rows', () => {
const nowMs = Date.now();
db!
Expand Down
9 changes: 3 additions & 6 deletions src/main/ipc/reticulum-db-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,9 @@ export function registerReticulumDbIpcHandlers({ ipcMain }: ReticulumDbIpcDeps):
? Math.trunc(Number(m.next_delivery_attempt_at))
: null;

if (
messageHash &&
!messageHash.startsWith('reticulum-pending-') &&
deliveryStatus &&
deliveryStatus !== 'sending'
) {
// Drop optimistic twins even while still `sending` — otherwise a stuck Direct
// Completes leaves reticulum-pending-* + real-hash duplicates after hydrate.
if (messageHash && !messageHash.startsWith('reticulum-pending-')) {
db.prepareOnce(
`DELETE FROM reticulum_messages
WHERE identity_id = ? AND sender_id = ? AND payload = ?
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/lib/reticulum/reticulumOutboundRetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ describe('shouldDeletePriorReticulumOutboundHash', () => {
expect(shouldDeletePriorReticulumOutboundHash('aa'.repeat(32), 'bb'.repeat(32))).toBe(true);
});

it('keeps optimistic pending rows (no prior SQLite hash)', () => {
it('deletes optimistic pending rows when rekeyed to a real hash', () => {
expect(shouldDeletePriorReticulumOutboundHash('reticulum-pending-1', 'bb'.repeat(32))).toBe(
false,
true,
);
});

it('skips when pending id already matches the new hash', () => {
const hash = 'cc'.repeat(32);
expect(shouldDeletePriorReticulumOutboundHash(hash, hash)).toBe(false);
});

it('skips empty ids', () => {
expect(shouldDeletePriorReticulumOutboundHash('', 'bb'.repeat(32))).toBe(false);
expect(shouldDeletePriorReticulumOutboundHash('reticulum-pending-1', '')).toBe(false);
});
});
9 changes: 5 additions & 4 deletions src/renderer/lib/reticulum/reticulumOutboundRetry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/**
* After a manual LXMF resend succeeds, the store rekeys `pendingId` → `newHash`.
* Delete the prior SQLite row when `pendingId` was a real LXMF hash (not an optimistic
* `reticulum-pending-*` id), so failed+retried messages do not leave duplicate DB rows.
* After LXMF send (or resend) rekeys `pendingId` → `newHash`, delete the prior SQLite row.
* Covers:
* - optimistic `reticulum-pending-*` rows (otherwise orphan while still `sending`)
* - prior real LXMF hashes after a successful retry
*/
export function shouldDeletePriorReticulumOutboundHash(
pendingId: string,
newHash: string,
): boolean {
return pendingId !== newHash && !pendingId.startsWith('reticulum-pending-');
return pendingId !== newHash && pendingId.length > 0 && newHash.length > 0;
}