Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-15 - [Initial Bottleneck Hunting]
**Learning:** Found a performance anti-pattern in IndexedDB usage where multiple notes are saved individually, creating N separate transactions instead of one. Also noticed that PBKDF2 is used per note with unique salts, which is secure but expensive during initial load.
**Action:** Implement a bulk save function for IndexedDB to use a single transaction when loading multiple notes from the server.
35 changes: 11 additions & 24 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions frontend/src/intents/notesIntents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { apiClient } from '../services/api';
import { encrypt, decrypt } from '../services/encryption';
import {
saveNoteToDB,
saveNotesToDB,
getNotesFromDB,
deleteNoteFromDB,
addToSyncQueue as addToSyncQueueDB,
Expand Down Expand Up @@ -82,10 +83,8 @@ export const loadNotesIntent = createAsyncThunk(
})
);

// Save to IndexedDB
for (const note of decryptedNotes) {
await saveNoteToDB(note);
}
// Save to IndexedDB (optimized bulk save)
await saveNotesToDB(decryptedNotes);

dispatch(setNotes(decryptedNotes));
dispatch(setLoading(false));
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/services/__tests__/offline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Unit tests for offline service
*/

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { saveNoteToDB, saveNotesToDB, getNotesFromDB } from '../offline';
import type { Note } from '../../models/types';

// Mock the idb openDB
vi.mock('idb', () => ({
openDB: vi.fn(),
}));

import { openDB } from 'idb';

describe('Offline Service', () => {
const mockNote: Note = {
id: '1',
title: 'Test Note',
content: 'Test Content',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const mockDb = {
put: vi.fn(),
getAll: vi.fn(),
transaction: vi.fn(),
};

const mockStore = {
put: vi.fn(),
clear: vi.fn(),
};

const mockTx = {
objectStore: vi.fn(() => mockStore),
done: Promise.resolve(),
};

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(openDB).mockResolvedValue(mockDb as any);
mockDb.transaction.mockReturnValue(mockTx as any);
});

it('should save a single note', async () => {
await saveNoteToDB(mockNote);
expect(mockDb.put).toHaveBeenCalledWith('notes', mockNote);
});

it('should save multiple notes in a single transaction', async () => {
const notes = [mockNote, { ...mockNote, id: '2' }];
await saveNotesToDB(notes);

expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(mockDb.transaction).toHaveBeenCalledWith('notes', 'readwrite');
expect(mockTx.objectStore).toHaveBeenCalledWith('notes');
expect(mockStore.put).toHaveBeenCalledTimes(2);
expect(mockStore.put).toHaveBeenCalledWith(notes[0]);
expect(mockStore.put).toHaveBeenCalledWith(notes[1]);
Comment on lines +57 to +61
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test claims to verify β€œsingle transaction” behavior, but it doesn’t assert the transaction call count. toHaveBeenCalledWith will still pass if transaction('notes','readwrite') is invoked multiple times. Add an explicit toHaveBeenCalledTimes(1) (or equivalent) so this test would fail if the implementation regresses to multiple transactions.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added expect(mockDb.transaction).toHaveBeenCalledTimes(1) assertion before the existing toHaveBeenCalledWith check to explicitly verify single-transaction usage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. I've added the expect(mockDb.transaction).toHaveBeenCalledTimes(1) assertion to the test to explicitly verify that only one transaction is used for bulk operations.

});

it('should get all notes', async () => {
const notes = [mockNote];
mockDb.getAll.mockResolvedValue(notes);

const result = await getNotesFromDB();
expect(result).toEqual(notes);
expect(mockDb.getAll).toHaveBeenCalledWith('notes');
});

it('should return early if saving empty notes array', async () => {
await saveNotesToDB([]);
expect(mockDb.transaction).not.toHaveBeenCalled();
});
});
15 changes: 15 additions & 0 deletions frontend/src/services/offline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ export async function saveNoteToDB(note: Note): Promise<void> {
await database.put('notes', note);
}

/**
* Save multiple notes to IndexedDB in a single transaction
* Optimization: Using a single transaction is much faster than multiple individual puts
*/
export async function saveNotesToDB(notes: Note[]): Promise<void> {
if (notes.length === 0) return;
const database = await initDB();
const tx = database.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
for (const note of notes) {
store.put(note);
}
await tx.done;
}

/**
* Get all notes from IndexedDB
*/
Expand Down