feat(07-01): implement core reducers with message deduplication logic

GREEN phase of TDD for Task 2:
- Implement addMessageFromWebSocket with full logic:
  * getChannelKey helper for session vs global channel keys
  * Channel initialization on first message
  * Message deduplication by msg_id using Array.some()
  * Sort messages by createdAt after insertion
  * Unread increment: only if window closed OR viewing different channel
- Implement setActiveChannel: sets activeChannel + channelType
- Implement openChatWindow: sets isWindowOpen, resets unread count, updates lastReadAt
- Implement closeChatWindow: sets isWindowOpen false only
- Export all 4 action creators

All 28 tests pass. Message deduplication validated.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-27 08:06:21 +05:30
parent 60a559e58f
commit 15a658a5dc
1 changed files with 84 additions and 2 deletions

View File

@ -31,13 +31,95 @@ const initialState = {
windowPosition: null
};
/**
* Helper function to get channel key from message
* Session messages use 'session-{sessionId}', global uses 'global'
* @param {Object} message - Message object
* @returns {string} Channel key
*/
const getChannelKey = (message) => {
if (message.channel === 'session' && message.sessionId) {
return message.sessionId;
}
if (message.channel === 'lesson' && message.lessonSessionId) {
return message.lessonSessionId;
}
return message.channel; // 'global'
};
const sessionChatSlice = createSlice({
name: 'sessionChat',
initialState,
reducers: {
// Reducers will be added in Tasks 2-3
/**
* Add message from WebSocket
* Handles deduplication, sorting, and unread count increment
*/
addMessageFromWebSocket: (state, action) => {
const message = action.payload;
const channel = getChannelKey(message);
// Initialize channel if not exists
if (!state.messagesByChannel[channel]) {
state.messagesByChannel[channel] = [];
}
// Deduplicate by msg_id
const exists = state.messagesByChannel[channel].some(m => m.id === message.id);
if (exists) return;
// Add message
state.messagesByChannel[channel].push(message);
// Sort by createdAt ASC
state.messagesByChannel[channel].sort((a, b) =>
new Date(a.createdAt) - new Date(b.createdAt)
);
// Increment unread count if window closed OR viewing different channel
if (!state.isWindowOpen || state.activeChannel !== channel) {
state.unreadCounts[channel] = (state.unreadCounts[channel] || 0) + 1;
}
},
/**
* Set active channel for viewing
*/
setActiveChannel: (state, action) => {
const { channel, channelType } = action.payload;
state.activeChannel = channel;
state.channelType = channelType;
},
/**
* Open chat window
* Resets unread count for active channel
*/
openChatWindow: (state) => {
state.isWindowOpen = true;
// Reset unread count for active channel
if (state.activeChannel) {
state.unreadCounts[state.activeChannel] = 0;
state.lastReadAt[state.activeChannel] = new Date().toISOString();
}
},
/**
* Close chat window
* Does not affect unread counts
*/
closeChatWindow: (state) => {
state.isWindowOpen = false;
}
}
});
export const {} = sessionChatSlice.actions;
export const {
addMessageFromWebSocket,
setActiveChannel,
openChatWindow,
closeChatWindow
} = sessionChatSlice.actions;
export default sessionChatSlice.reducer;