From 195c03354724b10db21f1b6618851de268f2ac82 Mon Sep 17 00:00:00 2001 From: silver-it Date: Thu, 14 Mar 2024 18:56:18 +0800 Subject: [PATCH] feat: update workflow considering the potential deadlock --- frontend/controller/actions/group.js | 55 ++++++++++++++++++++++++++++ frontend/main.js | 2 +- frontend/model/contracts/group.js | 25 ++----------- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index dc67824c7c..bf81450d62 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -897,6 +897,61 @@ export default (sbp('sbp/selectors/register', { sbp('okTurtles.events/emit', OPEN_MODAL, 'AddMembers') } }, + 'gi.actions/group/removeUselessIdentityContracts': function ({ contractID, possiblyUselessContractIDs }) { + // NOTE: should remove the identity contracts which we don't need to sync anymore + // for users who don't have any common groups, and any common DMs + const interval = setInterval(() => { + const pending = Object.entries(sbp('okTurtles.eventQueue/queuedInvocations')) + .filter(([q]) => typeof q === 'string') + .flatMap(([, list]) => list) + + if (pending.length === 0) { + const rootState = sbp('state/vuex/state') + const rootGetters = sbp('state/vuex/getters') + + const identityContractsMapToKeepSyncing = {} + const anotherGroupIDs = Object.keys(rootState.contracts) + .filter(gID => rootState.contracts[gID].type === 'gi.contracts/group' && gID !== contractID) + anotherGroupIDs.forEach(gID => { + for (const [iID] of Object.entries((rootState[gID].profiles || {}))) { + identityContractsMapToKeepSyncing[iID] = true + } + }) + // NOTE: we can not use 'directMessagesByGroup' getter since + // the group contract whose ID is contractID could be already removed + const groupDirectMessages = {} + for (const chatRoomId of Object.keys(rootGetters.ourDirectMessages)) { + const chatRoomState = rootState[chatRoomId] + + // NOTE: skip DMs whose chatroom contracts are not synced yet + if (!chatRoomState || !chatRoomState.members?.[rootGetters.ourIdentityContractId]) { + continue + } + // NOTE: direct messages should be filtered to the ones which are visible and of active group members + const members = Object.keys(chatRoomState.members) + const partners = members.filter(memberID => memberID !== rootGetters.ourIdentityContractId) + const hasActiveMember = partners.some(memberID => possiblyUselessContractIDs.includes(memberID)) + + if (hasActiveMember) { + groupDirectMessages[chatRoomId] = { partners } + } + } + + for (const cID of Object.keys(groupDirectMessages)) { + groupDirectMessages[cID].partners.forEach(iID => { + identityContractsMapToKeepSyncing[iID] = true + }) + } + + const identityContractsToRemove = possiblyUselessContractIDs.filter(cID => !identityContractsMapToKeepSyncing[cID]) + sbp('chelonia/contract/remove', identityContractsToRemove).catch(e => { + console.error(`[gi.actions/group/removeUselessIdentityContracts]: ${e.name} thrown by /remove useless identity contracts:`, e) + }) + + clearInterval(interval) + } + }, 1000) + }, ...encryptedAction('gi.actions/group/leaveChatRoom', L('Failed to leave chat channel.')), ...encryptedAction('gi.actions/group/deleteChatRoom', L('Failed to delete chat channel.')), ...encryptedAction('gi.actions/group/invite', L('Failed to create invite.')), diff --git a/frontend/main.js b/frontend/main.js index 53c61a796b..973111d435 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -121,7 +121,7 @@ async function startApp () { 'chelonia/contract/successfulKeySharesByContractID', 'gi.actions/chatroom/leave', 'gi.actions/group/removeOurselves', 'gi.actions/group/groupProfileUpdate', 'gi.actions/group/displayMincomeChangedPrompt', 'gi.actions/group/addChatRoom', - 'gi.actions/group/join', 'gi.actions/group/joinChatRoom', + 'gi.actions/group/join', 'gi.actions/group/joinChatRoom', 'gi.actions/group/removeUselessIdentityContracts', 'gi.actions/identity/addJoinDirectMessageKey', 'gi.actions/identity/leaveGroup', 'gi.notifications/emit', 'gi.actions/out/rotateKeys', 'gi.actions/group/shareNewKeys', 'gi.actions/chatroom/shareNewKeys', 'gi.actions/identity/shareNewPEK', diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index aa135b1929..eb170fde14 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -1894,27 +1894,10 @@ sbp('chelonia/defineContract', { }) if (memberID === identityContractID) { - // NOTE: should remove the identity contracts which we don't need to sync anymore - // for users who don't have any common groups, and any common DMs - const anotherGroupIDs = Object.keys(rootState.contracts) - .filter(gID => rootState.contracts[gID].type === 'gi.contracts/group' && gID !== contractID) - const identityContractsMapToKeepSyncing = {} - anotherGroupIDs.forEach(gID => { - for (const [iID] of Object.entries((rootState[gID].profiles || {}))) { - identityContractsMapToKeepSyncing[iID] = true - } - }) - const groupDirectMessages = rootGetters.directMessagesByGroup(contractID) - for (const cID of Object.keys(groupDirectMessages)) { - groupDirectMessages[cID].partners.forEach(iID => { - identityContractsMapToKeepSyncing[iID] = true - }) - } - - const identityContractsToRemove = Object.keys(state.profiles || {}) - .filter(cID => cID !== identityContractID && !identityContractsMapToKeepSyncing[cID]) - sbp('chelonia/contract/remove', identityContractsToRemove).catch(e => { - console.error(`sideEffect(removeMember): ${e.name} thrown by /remove useless identity contracts:`, e) + const possiblyUselessContractIDs = Object.keys(state.profiles || {}).filter(cID => cID !== identityContractID) + sbp('gi.actions/group/removeUselessIdentityContracts', { + contractID, + possiblyUselessContractIDs }) // we can't await on this in here, because it will cause a deadlock, since Chelonia processes