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:
parent
b0b4ae1a53
commit
4f5339d7eb
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue