diff --git a/lib/ops/dm-operations-store-ops.js b/lib/ops/dm-operations-store-ops.js --- a/lib/ops/dm-operations-store-ops.js +++ b/lib/ops/dm-operations-store-ops.js @@ -322,6 +322,16 @@ item => !op.payload.ids.includes(item.id), ), }; + } else if (op.type === 'replace_dm_operation') { + if (op.payload.type === dmOperationUnshimmedType) { + processedStore = { + ...processedStore, + shimmedOperations: [ + ...processedStore.shimmedOperations, + { operation: op.payload.operation, id: op.payload.id }, + ], + }; + } } } @@ -435,6 +445,103 @@ }, }; +export function convertQueuedDMOperationsStoreToAddOps( + store: QueuedDMOperations, +): $ReadOnlyArray { + const operations: Array = []; + + // Convert thread queue operations + for (const [threadID, operationsQueue] of Object.entries(store.threadQueue)) { + for (const { operation, timestamp } of operationsQueue) { + operations.push({ + type: 'add_queued_dm_operation', + payload: { + condition: { + type: queuedDMOperationConditionType.THREAD, + threadID, + }, + operation, + timestamp, + }, + }); + } + } + + // Convert message queue operations + for (const [messageID, operationsQueue] of Object.entries( + store.messageQueue, + )) { + for (const { operation, timestamp } of operationsQueue) { + operations.push({ + type: 'add_queued_dm_operation', + payload: { + condition: { + type: queuedDMOperationConditionType.MESSAGE, + messageID, + }, + operation, + timestamp, + }, + }); + } + } + + // Convert entry queue operations + for (const [entryID, operationsQueue] of Object.entries(store.entryQueue)) { + for (const { operation, timestamp } of operationsQueue) { + operations.push({ + type: 'add_queued_dm_operation', + payload: { + condition: { + type: queuedDMOperationConditionType.ENTRY, + entryID, + }, + operation, + timestamp, + }, + }); + } + } + + // Convert membership queue operations + for (const [threadID, threadMembershipQueue] of Object.entries( + store.membershipQueue, + )) { + for (const [userID, operationsQueue] of Object.entries( + threadMembershipQueue, + )) { + for (const { operation, timestamp } of operationsQueue) { + operations.push({ + type: 'add_queued_dm_operation', + payload: { + condition: { + type: queuedDMOperationConditionType.MEMBERSHIP, + threadID, + userID, + }, + operation, + timestamp, + }, + }); + } + } + } + + // Convert shimmed operations + for (const shimmedOp of store.shimmedOperations) { + operations.push({ + type: 'replace_dm_operation', + payload: { + id: shimmedOp.id, + type: dmOperationUnshimmedType, + operation: shimmedOp.operation, + }, + }); + } + + return operations; +} + export const dmOperationUnshimmedType = 'unshimmed_operation'; export { diff --git a/lib/ops/dm-operations-store-ops.test.js b/lib/ops/dm-operations-store-ops.test.js new file mode 100644 --- /dev/null +++ b/lib/ops/dm-operations-store-ops.test.js @@ -0,0 +1,170 @@ +// @flow + +import { + convertQueuedDMOperationsStoreToAddOps, + dmOperationsStoreOpsHandlers, +} from './dm-operations-store-ops.js'; +import type { QueuedDMOperations } from '../types/dm-ops.js'; + +describe('convertQueuedDMOperationsStoreToAddOps', () => { + // Shared mock operation for reuse + const mockOperation = { + type: 'send_text_message', + threadID: 'thread1', + creatorID: 'user123', + time: 1642500000000, + messageID: 'msg1', + text: 'Mock operation', + }; + + it('should convert complete store to operations and back', () => { + // Create mock store with data for all queue types + const originalStore: QueuedDMOperations = { + threadQueue: { + thread1: [ + { + operation: mockOperation, + timestamp: 1642500000000, + }, + { + operation: { ...mockOperation, messageID: 'msg1b' }, + timestamp: 1642500001000, + }, + ], + thread2: [ + { + operation: { + ...mockOperation, + threadID: 'thread2', + messageID: 'msg2a', + }, + timestamp: 1642500002000, + }, + ], + }, + messageQueue: { + msg3: [ + { + operation: mockOperation, + timestamp: 1642500003000, + }, + { + operation: { ...mockOperation, messageID: 'msg3b' }, + timestamp: 1642500004000, + }, + ], + msg4: [ + { + operation: { ...mockOperation, messageID: 'msg4a' }, + timestamp: 1642500005000, + }, + ], + }, + entryQueue: { + entry1: [ + { + operation: mockOperation, + timestamp: 1642500006000, + }, + { + operation: { ...mockOperation, messageID: 'entry1b' }, + timestamp: 1642500007000, + }, + ], + entry2: [ + { + operation: { ...mockOperation, messageID: 'entry2a' }, + timestamp: 1642500008000, + }, + ], + }, + membershipQueue: { + thread1: { + user123: [ + { + operation: mockOperation, + timestamp: 1642500009000, + }, + { + operation: { ...mockOperation, messageID: 'member1b' }, + timestamp: 1642500010000, + }, + ], + user456: [ + { + operation: { ...mockOperation, messageID: 'member2a' }, + timestamp: 1642500011000, + }, + ], + }, + thread2: { + user789: [ + { + operation: { + ...mockOperation, + threadID: 'thread2', + messageID: 'member3a', + }, + timestamp: 1642500012000, + }, + ], + }, + }, + shimmedOperations: [ + { + id: 'shimmed1', + operation: mockOperation, + }, + { + id: 'shimmed2', + operation: mockOperation, + }, + ], + }; + + // Convert store to operations + const operations = convertQueuedDMOperationsStoreToAddOps(originalStore); + + // Verify we have the correct number of operations + // 3 thread + 3 message + 3 entry + 4 membership + 2 shimmed = 15 total + expect(operations).toHaveLength(15); + + // Process operations back to store + const emptyStore = { + threadQueue: {}, + messageQueue: {}, + entryQueue: {}, + membershipQueue: {}, + shimmedOperations: [], + }; + + const reconstructedStore = + dmOperationsStoreOpsHandlers.processStoreOperations( + emptyStore, + operations, + ); + + // Verify reconstructed store matches original + expect(reconstructedStore).toEqual(originalStore); + }); + + it('should handle empty store', () => { + const emptyStore = { + threadQueue: {}, + messageQueue: {}, + entryQueue: {}, + membershipQueue: {}, + shimmedOperations: [], + }; + + const operations = convertQueuedDMOperationsStoreToAddOps(emptyStore); + expect(operations).toHaveLength(0); + + const reconstructedStore = + dmOperationsStoreOpsHandlers.processStoreOperations( + emptyStore, + operations, + ); + expect(reconstructedStore).toEqual(emptyStore); + }); +}); diff --git a/native/redux/handle-redux-migration-failure.js b/native/redux/handle-redux-migration-failure.js --- a/native/redux/handle-redux-migration-failure.js +++ b/native/redux/handle-redux-migration-failure.js @@ -30,6 +30,7 @@ 'globalThemeInfo', 'holderStore', 'threadActivityStore', + 'queuedDMOperations', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { diff --git a/native/redux/persist-constants.js b/native/redux/persist-constants.js --- a/native/redux/persist-constants.js +++ b/native/redux/persist-constants.js @@ -5,6 +5,6 @@ // NOTE: renaming this constant requires updating // `native/native_rust_library/build.rs` to correctly // scrap Redux state version from this file. -const storeVersion = 94; +const storeVersion = 95; export { rootKey, storeVersion }; diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -17,6 +17,7 @@ convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; +import { convertQueuedDMOperationsStoreToAddOps } from 'lib/ops/dm-operations-store-ops.js'; import type { ClientDBEntryStoreOperation, ReplaceEntryOperation, @@ -1690,6 +1691,17 @@ }, }; }: MigrationFunction), + [95]: (async (state: AppState) => { + const queuedDMOperations = state.queuedDMOperations; + + return { + state, + ops: { + dmOperationStoreOperations: + convertQueuedDMOperationsStoreToAddOps(queuedDMOperations), + }, + }; + }: MigrationFunction), }); const persistConfig = { diff --git a/web/redux/handle-redux-migration-failure.js b/web/redux/handle-redux-migration-failure.js --- a/web/redux/handle-redux-migration-failure.js +++ b/web/redux/handle-redux-migration-failure.js @@ -11,7 +11,6 @@ 'customServer', 'messageStore', 'tunnelbrokerDeviceToken', - 'queuedDMOperations', 'restoreBackupState', ]; diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js --- a/web/redux/persist-constants.js +++ b/web/redux/persist-constants.js @@ -3,6 +3,6 @@ const rootKey = 'root'; const rootKeyPrefix = 'persist:'; const completeRootKey = `${rootKeyPrefix}${rootKey}`; -const storeVersion = 94; +const storeVersion = 95; export { rootKey, rootKeyPrefix, completeRootKey, storeVersion }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -7,6 +7,7 @@ import type { PersistConfig } from 'redux-persist/src/types.js'; import { createReplaceThreadOperation } from 'lib/ops/create-replace-thread-operation.js'; +import { convertQueuedDMOperationsStoreToAddOps } from 'lib/ops/dm-operations-store-ops.js'; import { type HolderStoreOperation, createReplaceHoldersOperation, @@ -904,6 +905,17 @@ }, }; }: MigrationFunction), + [95]: (async (state: AppState) => { + const queuedDMOperations = state.queuedDMOperations; + + return { + state, + ops: { + dmOperationStoreOperations: + convertQueuedDMOperationsStoreToAddOps(queuedDMOperations), + }, + }; + }: MigrationFunction), }; const persistConfig: PersistConfig = {