-
Notifications
You must be signed in to change notification settings - Fork 4
feat: paginate History tab to prevent resource exhaustion #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e133471
2cb31d3
fbf6faa
c29aa1c
784503b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -693,6 +693,7 @@ <h2>History</h2> | |||||||||||||||||||||||||||||||||||
| <button class="secondary" onclick="showAddModal()">+ Add Manual</button> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| <div id="history-list"></div> | ||||||||||||||||||||||||||||||||||||
| <button id="history-load-more" class="secondary" style="display:none; width:100%; margin-top:1rem;">Load More</button> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <!-- Reports View --> | ||||||||||||||||||||||||||||||||||||
|
|
@@ -2833,80 +2834,109 @@ <h2 id="edit-modal-title">Edit Pomodoro</h2> | |||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // History | ||||||||||||||||||||||||||||||||||||
| async function loadHistory() { | ||||||||||||||||||||||||||||||||||||
| // History — paginated | ||||||||||||||||||||||||||||||||||||
| const HISTORY_PAGE_SIZE = 50; | ||||||||||||||||||||||||||||||||||||
| let historyState = { lastTimestamp: null, hasMore: false, todoMap: null, lastDateGroup: null }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| document.getElementById('history-load-more').addEventListener('click', () => loadHistory(false)); | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent duplicate items from being loaded and rendered if a user clicks the 'Load More' button multiple times quickly, we should disable the button while the asynchronous
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| async function loadHistory(reset = true) { | ||||||||||||||||||||||||||||||||||||
| const list = document.getElementById('history-list'); | ||||||||||||||||||||||||||||||||||||
| const loadMoreBtn = document.getElementById('history-load-more'); | ||||||||||||||||||||||||||||||||||||
| const tz = getEffectiveTimezone(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const pomodoros = await Storage.getPomodoros(); | ||||||||||||||||||||||||||||||||||||
| if (reset) { | ||||||||||||||||||||||||||||||||||||
| historyState = { lastTimestamp: null, hasMore: false, todoMap: null, lastDateGroup: null }; | ||||||||||||||||||||||||||||||||||||
| list.innerHTML = ''; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Build todo map once per reset | ||||||||||||||||||||||||||||||||||||
| if (!historyState.todoMap) { | ||||||||||||||||||||||||||||||||||||
| historyState.todoMap = {}; | ||||||||||||||||||||||||||||||||||||
| const allTodos = await Storage.getTodos(); | ||||||||||||||||||||||||||||||||||||
| const allLists = await Storage.getTodoLists(); | ||||||||||||||||||||||||||||||||||||
| for (const t of allTodos) { | ||||||||||||||||||||||||||||||||||||
| const listName = allLists.find(l => l.id === t.list_id)?.name; | ||||||||||||||||||||||||||||||||||||
| historyState.todoMap[t.id] = listName ? `${listName} > ${t.title}` : t.title; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(pomodoros) || pomodoros.length === 0) { | ||||||||||||||||||||||||||||||||||||
| const { pomodoros, hasMore } = await Storage.getPomodorosPage(HISTORY_PAGE_SIZE, historyState.lastTimestamp); | ||||||||||||||||||||||||||||||||||||
| historyState.hasMore = hasMore; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (reset && pomodoros.length === 0) { | ||||||||||||||||||||||||||||||||||||
| list.innerHTML = '<div class="empty">No pomodoros yet. Start your first timer!</div>'; | ||||||||||||||||||||||||||||||||||||
| loadMoreBtn.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Build todo title map for linked pomodoros | ||||||||||||||||||||||||||||||||||||
| const historyTodoMap = {}; | ||||||||||||||||||||||||||||||||||||
| const allTodos = await Storage.getTodos(); | ||||||||||||||||||||||||||||||||||||
| const allLists = await Storage.getTodoLists(); | ||||||||||||||||||||||||||||||||||||
| for (const t of allTodos) { | ||||||||||||||||||||||||||||||||||||
| const listName = allLists.find(l => l.id === t.list_id)?.name; | ||||||||||||||||||||||||||||||||||||
| historyTodoMap[t.id] = listName ? `${listName} > ${t.title}` : t.title; | ||||||||||||||||||||||||||||||||||||
| if (pomodoros.length > 0) { | ||||||||||||||||||||||||||||||||||||
| historyState.lastTimestamp = pomodoros[pomodoros.length - 1].start_time; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Group by date (using configured timezone) | ||||||||||||||||||||||||||||||||||||
| // Group this page by date | ||||||||||||||||||||||||||||||||||||
| const grouped = {}; | ||||||||||||||||||||||||||||||||||||
| const groupOrder = []; | ||||||||||||||||||||||||||||||||||||
| pomodoros.forEach(p => { | ||||||||||||||||||||||||||||||||||||
| const date = formatDateWithDayTz(p.start_time, tz); | ||||||||||||||||||||||||||||||||||||
| if (!grouped[date]) grouped[date] = []; | ||||||||||||||||||||||||||||||||||||
| if (!grouped[date]) { grouped[date] = []; groupOrder.push(date); } | ||||||||||||||||||||||||||||||||||||
| grouped[date].push(p); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let html = ''; | ||||||||||||||||||||||||||||||||||||
| for (const [date, items] of Object.entries(grouped)) { | ||||||||||||||||||||||||||||||||||||
| // Sort items within this day by start time (morning to evening) | ||||||||||||||||||||||||||||||||||||
| // Render and append | ||||||||||||||||||||||||||||||||||||
| const fragment = document.createDocumentFragment(); | ||||||||||||||||||||||||||||||||||||
| for (const date of groupOrder) { | ||||||||||||||||||||||||||||||||||||
| const items = grouped[date]; | ||||||||||||||||||||||||||||||||||||
| // Within each day, sort morning to evening for display | ||||||||||||||||||||||||||||||||||||
| items.sort((a, b) => new Date(a.start_time) - new Date(b.start_time)); | ||||||||||||||||||||||||||||||||||||
| html += `<h3>${date}</h3>`; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // If this date matches the last group already on screen, don't add a new header | ||||||||||||||||||||||||||||||||||||
| if (date !== historyState.lastDateGroup) { | ||||||||||||||||||||||||||||||||||||
| const h3 = document.createElement('h3'); | ||||||||||||||||||||||||||||||||||||
| h3.textContent = date; | ||||||||||||||||||||||||||||||||||||
| fragment.appendChild(h3); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| historyState.lastDateGroup = date; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| items.forEach(p => { | ||||||||||||||||||||||||||||||||||||
| const color = TYPE_COLORS[p.type] || '#6b7280'; | ||||||||||||||||||||||||||||||||||||
| const time = new Date(p.start_time).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: tz }); | ||||||||||||||||||||||||||||||||||||
| const safeId = escapeHtml(p.id); | ||||||||||||||||||||||||||||||||||||
| const linkedInfo = p.linked_todo_id && historyTodoMap[p.linked_todo_id] | ||||||||||||||||||||||||||||||||||||
| ? ` · <span style="color:var(--success);">${escapeHtml(historyTodoMap[p.linked_todo_id])}</span>` : ''; | ||||||||||||||||||||||||||||||||||||
| html += ` | ||||||||||||||||||||||||||||||||||||
| <div class="pomodoro-item" data-id="${safeId}"> | ||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||
| <span class="pomodoro-name">${escapeHtml(p.name)}</span> | ||||||||||||||||||||||||||||||||||||
| <span class="pomodoro-type" style="background:${color}20;color:${color}">${escapeHtml(p.type)}</span> | ||||||||||||||||||||||||||||||||||||
| <div class="pomodoro-meta">${time} - ${p.duration_minutes} min${linkedInfo}</div> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| <div class="pomodoro-actions"> | ||||||||||||||||||||||||||||||||||||
| <button class="delete-btn" title="Delete">✕</button> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| const linkedInfo = p.linked_todo_id && historyState.todoMap[p.linked_todo_id] | ||||||||||||||||||||||||||||||||||||
| ? ` · <span style="color:var(--success);">${escapeHtml(historyState.todoMap[p.linked_todo_id])}</span>` : ''; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const div = document.createElement('div'); | ||||||||||||||||||||||||||||||||||||
| div.className = 'pomodoro-item'; | ||||||||||||||||||||||||||||||||||||
| div.dataset.id = safeId; | ||||||||||||||||||||||||||||||||||||
| div.innerHTML = ` | ||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||
| <span class="pomodoro-name">${escapeHtml(p.name)}</span> | ||||||||||||||||||||||||||||||||||||
| <span class="pomodoro-type" style="background:${color}20;color:${color}">${escapeHtml(p.type)}</span> | ||||||||||||||||||||||||||||||||||||
| <div class="pomodoro-meta">${time} - ${p.duration_minutes} min${linkedInfo}</div> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| <div class="pomodoro-actions"> | ||||||||||||||||||||||||||||||||||||
| <button class="delete-btn" title="Delete">✕</button> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||
| div.addEventListener('click', () => { | ||||||||||||||||||||||||||||||||||||
| document.querySelectorAll('.pomodoro-item.highlighted').forEach(el => el.classList.remove('highlighted')); | ||||||||||||||||||||||||||||||||||||
| div.classList.add('highlighted'); | ||||||||||||||||||||||||||||||||||||
| showEditModal(safeId); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| div.querySelector('.delete-btn').addEventListener('click', (e) => { | ||||||||||||||||||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||||||||||||||||||
| deletePomodoro(safeId); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| fragment.appendChild(div); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| list.innerHTML = html; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Attach event listeners - click item to edit, delete button to delete | ||||||||||||||||||||||||||||||||||||
| list.querySelectorAll('.pomodoro-item').forEach(item => { | ||||||||||||||||||||||||||||||||||||
| const id = item.dataset.id; | ||||||||||||||||||||||||||||||||||||
| // Click anywhere on item to edit (with highlight) | ||||||||||||||||||||||||||||||||||||
| item.addEventListener('click', () => { | ||||||||||||||||||||||||||||||||||||
| document.querySelectorAll('.pomodoro-item.highlighted').forEach(el => el.classList.remove('highlighted')); | ||||||||||||||||||||||||||||||||||||
| item.classList.add('highlighted'); | ||||||||||||||||||||||||||||||||||||
| showEditModal(id); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| // Delete button stops propagation so it doesn't trigger edit | ||||||||||||||||||||||||||||||||||||
| item.querySelector('.delete-btn').addEventListener('click', (e) => { | ||||||||||||||||||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||||||||||||||||||
| deletePomodoro(id); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| list.appendChild(fragment); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2887
to
+2934
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a single day's pomodoros are split across page boundaries, descending pagination loads the newer items (evening) on page 1 and the older items (morning) on page 2. Because page 2 is appended to the end of the DOM, the older items are rendered after the newer items, completely breaking the morning-to-evening chronological order within that day (e.g., showing 13:00, 15:00, then 09:00, 11:00). Wrapping each day's items in a container allows us to easily append and re-sort them chronologically. // Render and append
const fragment = document.createDocumentFragment();
for (const date of groupOrder) {
const items = grouped[date];
// Find or create the day group container
let dayGroup = list.querySelector(`.day-group[data-date="${date}"]`) || fragment.querySelector(`.day-group[data-date="${date}"]`);
let dayItemsContainer;
if (!dayGroup) {
dayGroup = document.createElement('div');
dayGroup.className = 'day-group';
dayGroup.dataset.date = date;
const h3 = document.createElement('h3');
h3.textContent = date;
dayGroup.appendChild(h3);
dayItemsContainer = document.createElement('div');
dayItemsContainer.className = 'day-items';
dayGroup.appendChild(dayItemsContainer);
fragment.appendChild(dayGroup);
} else {
dayItemsContainer = dayGroup.querySelector('.day-items');
}
items.forEach(p => {
const color = TYPE_COLORS[p.type] || '#6b7280';
const time = new Date(p.start_time).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: tz });
const safeId = escapeHtml(p.id);
const linkedInfo = p.linked_todo_id && historyState.todoMap[p.linked_todo_id]
? ` · <span style="color:var(--success);">${escapeHtml(historyState.todoMap[p.linked_todo_id])}</span>` : '';
const div = document.createElement('div');
div.className = 'pomodoro-item';
div.dataset.id = safeId;
div.dataset.startTime = p.start_time;
div.innerHTML = `
<div>
<span class="pomodoro-name">${escapeHtml(p.name)}</span>
<span class="pomodoro-type" style="background:${color}20;color:${color}">${escapeHtml(p.type)}</span>
<div class="pomodoro-meta">${time} - ${p.duration_minutes} min${linkedInfo}</div>
</div>
<div class="pomodoro-actions">
<button class="delete-btn" title="Delete">✕</button>
</div>
`;
div.addEventListener('click', () => {
document.querySelectorAll('.pomodoro-item.highlighted').forEach(el => el.classList.remove('highlighted'));
div.classList.add('highlighted');
showEditModal(safeId);
});
div.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
deletePomodoro(safeId);
});
dayItemsContainer.appendChild(div);
});
// Sort all items in this day container morning to evening
const sortedElements = Array.from(dayItemsContainer.children)
.sort((a, b) => new Date(a.dataset.startTime) - new Date(b.dataset.startTime));
dayItemsContainer.innerHTML = '';
sortedElements.forEach(el => dayItemsContainer.appendChild(el));
}
list.appendChild(fragment); |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| loadMoreBtn.style.display = hasMore ? '' : 'none'; | ||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||
| console.error('Error loading history:', e); | ||||||||||||||||||||||||||||||||||||
| list.innerHTML = '<div class="empty">Error loading history. Please refresh.</div>'; | ||||||||||||||||||||||||||||||||||||
| if (reset) list.innerHTML = '<div class="empty">Error loading history. Please refresh.</div>'; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -2925,19 +2955,21 @@ <h2 id="edit-modal-title">Edit Pomodoro</h2> | |||||||||||||||||||||||||||||||||||
| histBtn.setAttribute('tabindex', '0'); | ||||||||||||||||||||||||||||||||||||
| document.getElementById('history-view').classList.add('active'); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Load history and wait for it to complete | ||||||||||||||||||||||||||||||||||||
| // Load first page and search for the target pomodoro | ||||||||||||||||||||||||||||||||||||
| await loadHistory(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Find and highlight the pomodoro | ||||||||||||||||||||||||||||||||||||
| const item = document.querySelector(`.pomodoro-item[data-id="${CSS.escape(id)}"]`); | ||||||||||||||||||||||||||||||||||||
| let item = document.querySelector(`.pomodoro-item[data-id="${CSS.escape(id)}"]`); | ||||||||||||||||||||||||||||||||||||
| // Load up to 10 more pages (500 items) to find the target — cap prevents runaway loading | ||||||||||||||||||||||||||||||||||||
| let pagesLoaded = 0; | ||||||||||||||||||||||||||||||||||||
| while (!item && historyState.hasMore && pagesLoaded < 10) { | ||||||||||||||||||||||||||||||||||||
| await loadHistory(false); | ||||||||||||||||||||||||||||||||||||
| item = document.querySelector(`.pomodoro-item[data-id="${CSS.escape(id)}"]`); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2961
to
+2966
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If To prevent this infinite loop, we should check if any progress was made (i.e., if
Suggested change
|
||||||||||||||||||||||||||||||||||||
| pagesLoaded++; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (item) { | ||||||||||||||||||||||||||||||||||||
| // Remove any existing highlights | ||||||||||||||||||||||||||||||||||||
| document.querySelectorAll('.pomodoro-item.highlighted').forEach(el => el.classList.remove('highlighted')); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Scroll to the item | ||||||||||||||||||||||||||||||||||||
| item.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Add highlight (stays until another item is clicked) | ||||||||||||||||||||||||||||||||||||
| item.classList.add('highlighted'); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -4246,8 +4278,6 @@ <h2 id="edit-modal-title">Edit Pomodoro</h2> | |||||||||||||||||||||||||||||||||||
| updateGooglePomodoroCount(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Always load settings and plugins (independent of auth) | ||||||||||||||||||||||||||||||||||||
| await loadSettings(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Update storage mode indicator in header | ||||||||||||||||||||||||||||||||||||
|
|
@@ -5741,11 +5771,18 @@ <h2 id="edit-modal-title">Edit Pomodoro</h2> | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| (async () => { | ||||||||||||||||||||||||||||||||||||
| await checkAuthStatus(); | ||||||||||||||||||||||||||||||||||||
| await loadWeeklyOverview(); | ||||||||||||||||||||||||||||||||||||
| await loadHistory(); | ||||||||||||||||||||||||||||||||||||
| // Phase 1: Open IndexedDB and load settings (no network, sub-ms). | ||||||||||||||||||||||||||||||||||||
| // This lets the UI render with correct timezone/date format immediately. | ||||||||||||||||||||||||||||||||||||
| await Storage.openDatabase(); | ||||||||||||||||||||||||||||||||||||
| await loadSettings(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Phase 2: Run auth (network) and view rendering (IndexedDB) in parallel. | ||||||||||||||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||||||||||||
| checkAuthStatus(), | ||||||||||||||||||||||||||||||||||||
| loadWeeklyOverview() | ||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Load todos from cloud if logged in, then render | ||||||||||||||||||||||||||||||||||||
| // Phase 3: Sync cloud data then render todos once with fresh state. | ||||||||||||||||||||||||||||||||||||
| if (authStatus.logged_in) { | ||||||||||||||||||||||||||||||||||||
| await Storage.loadTodosFromCloud(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
IDBKeyRange.upperBound(beforeTimestamp, true)on a non-unique index likestart_timecan cause records to be skipped at page boundaries if multiple pomodoros have the exact samestart_time(which is common for imported or manually added entries). Since the upper bound is exclusive, any other pomodoros with the same timestamp as the cursor will be excluded from the next page.Consider documenting this limitation or eventually using a compound index (e.g.,
[start_time, id]) to ensure unique cursor-based pagination.