feat(07-02): implement fetchChatHistory async thunk with extra reducers

Add fetchChatHistory async thunk with complete lifecycle handling:
- pending: Sets loading status and clears errors
- fulfilled: Adds messages with deduplication and sorting
- rejected: Sets error status with error message
- Message deduplication by ID to prevent duplicates
- Chronological sorting (createdAt ASC) after prepending
- Pagination cursor storage in nextCursors state
- Handles both session and global channels

All 46 unit tests passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-27 08:16:26 +05:30
parent b0b4ae1a53
commit 4f5339d7eb
1 changed files with 61 additions and 1 deletions

View File

@ -1,4 +1,24 @@
import { createSlice } from '@reduxjs/toolkit';
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { getChatMessages } from '../../helpers/rest';
/**
* Async thunk to fetch chat history for a channel
* @param {Object} params - Request parameters
* @param {string} params.channel - Channel ID (session ID, lesson ID, or 'global')
* @param {string} params.sessionId - Session ID for session channels
* @param {number} [params.before] - Pagination cursor
*/
export const fetchChatHistory = createAsyncThunk(
'sessionChat/fetchChatHistory',
async ({ channel, sessionId, before }) => {
const response = await getChatMessages({
channel: sessionId ? 'session' : 'global',
sessionId,
before
});
return { channel, ...response };
}
);
/**
* Initial state for session chat
@ -139,6 +159,46 @@ const sessionChatSlice = createSlice({
setWindowPosition: (state, action) => {
state.windowPosition = action.payload;
}
},
extraReducers: (builder) => {
builder
// fetchChatHistory pending
.addCase(fetchChatHistory.pending, (state, action) => {
const channel = action.meta.arg.channel;
state.fetchStatus[channel] = 'loading';
state.fetchError[channel] = null;
})
// fetchChatHistory fulfilled
.addCase(fetchChatHistory.fulfilled, (state, action) => {
const channel = action.meta.arg.channel;
const { messages, next } = action.payload;
// Initialize channel if not exists
if (!state.messagesByChannel[channel]) {
state.messagesByChannel[channel] = [];
}
// Deduplicate messages
const existingIds = new Set(state.messagesByChannel[channel].map(m => m.id));
const newMessages = messages.filter(m => !existingIds.has(m.id));
// Prepend new messages (oldest first for pagination)
state.messagesByChannel[channel] = [...newMessages, ...state.messagesByChannel[channel]];
// Sort by createdAt ASC to maintain chronological order
state.messagesByChannel[channel].sort((a, b) =>
new Date(a.createdAt) - new Date(b.createdAt)
);
state.fetchStatus[channel] = 'succeeded';
state.nextCursors[channel] = next;
})
// fetchChatHistory rejected
.addCase(fetchChatHistory.rejected, (state, action) => {
const channel = action.meta.arg.channel;
state.fetchStatus[channel] = 'failed';
state.fetchError[channel] = action.error.message;
});
}
});