fix(messages-recv): use consistent transaction key for session operations
fix(messages-recv): use consistent transaction key for session operations
This commit is contained in:
@@ -465,10 +465,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
if (shouldRecreateSession) {
|
||||
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry')
|
||||
// Delete existing session to force recreation
|
||||
// CRITICAL: Wrap in transaction to prevent race with other session operations
|
||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
||||
await authState.keys.transaction(async () => {
|
||||
await authState.keys.set({ session: { [sessionId]: null } })
|
||||
}, `delete-session-${sessionId}`)
|
||||
}, authState.creds.me?.id || 'session-operation')
|
||||
forceIncludeKeys = true
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1026,10 +1027,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
if (shouldRecreateSession) {
|
||||
logger.debug({ participant, retryCount, reason: recreateReason, errorCode }, 'recreating session for outgoing retry')
|
||||
// CRITICAL: Wrap in transaction to prevent race with other session operations
|
||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
||||
await authState.keys.transaction(async () => {
|
||||
await authState.keys.set({ session: { [sessionId]: null } })
|
||||
}, `delete-session-${sessionId}`)
|
||||
}, authState.creds.me?.id || 'session-operation')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
|
||||
|
||||
+16
-5
@@ -303,11 +303,6 @@ export const addTransactionCapability = (
|
||||
isInTransaction,
|
||||
|
||||
transaction: async (work, key) => {
|
||||
// CRITICAL: Prevent transactions after destroy to avoid use-after-free
|
||||
if (destroyed) {
|
||||
throw new Error('Transaction capability destroyed - socket closed')
|
||||
}
|
||||
|
||||
const existing = txStorage.getStore()
|
||||
|
||||
// Nested transaction - reuse existing context
|
||||
@@ -322,6 +317,12 @@ export const addTransactionCapability = (
|
||||
|
||||
try {
|
||||
return await mutex.runExclusive(async () => {
|
||||
// CRITICAL: Check destroyed flag INSIDE mutex to prevent race condition
|
||||
// This ensures atomic check-and-execute: if we acquire mutex, resources exist
|
||||
if (destroyed) {
|
||||
throw new Error('Transaction capability destroyed - cannot initiate new transactions')
|
||||
}
|
||||
|
||||
const ctx: TransactionContext = {
|
||||
cache: {},
|
||||
mutations: {},
|
||||
@@ -352,9 +353,19 @@ export const addTransactionCapability = (
|
||||
/**
|
||||
* Cleanup all resources (queues, managers, mutexes)
|
||||
* Should be called during connection cleanup
|
||||
*
|
||||
* IMPORTANT BEHAVIOR:
|
||||
* - Always sets destroyed=true to prevent NEW transactions
|
||||
* - If mutexes are locked (active transactions), returns early WITHOUT destroying resources
|
||||
* - This creates intentional temporary inconsistent state:
|
||||
* * destroyed=true (new transactions rejected)
|
||||
* * resources exist (active transactions complete safely)
|
||||
* * resources cleaned up by GC after active transactions finish
|
||||
* - If no locked mutexes, destroys resources immediately
|
||||
*/
|
||||
destroy: () => {
|
||||
// CRITICAL: Set destroyed flag FIRST to prevent new transactions
|
||||
// Note: Flag is set even if early return occurs (see doc above)
|
||||
destroyed = true
|
||||
logger.debug('🗑️ Cleaning up transaction capability resources')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user