Skip to content

Commit

Permalink
feat: improve IndexedDB persistence handling
Browse files Browse the repository at this point in the history
This PR improves the IndexedDB persistence handling in the app, making it more reliable and robust. Key improvements include better browser environment detection, database verification, improved error handling, and proper cleanup processes. While the persistence works best in Safari, Chrome-specific issues are handled gracefully.

Changes:
- Add better browser environment detection
- Add database verification and testing
- Improve error handling and logging
- Add proper cleanup process
- Make persistence work reliably in Safari
- Handle Chrome-specific issues gracefully
- Add better state tracking and management

Note: For the best experience with persistence features, use Safari browser.
  • Loading branch information
vgcman16 committed Oct 30, 2024
1 parent fb156fa commit dadf2c1
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 57 deletions.
2 changes: 2 additions & 0 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export function Chat() {
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
hideProgressBar
autoClose={false}
/>
</>
) : null;
Expand Down
131 changes: 129 additions & 2 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,134 @@
import { RemixBrowser } from '@remix-run/react';
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { createScopedLogger } from '~/utils/logger';

startTransition(() => {
hydrateRoot(document.getElementById('root')!, <RemixBrowser />);
const logger = createScopedLogger('Client');

function isChrome(): boolean {
return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
}

// Initialize IndexedDB before hydration
async function initIndexedDB() {
if (typeof window === 'undefined' || !window.indexedDB) {
logger.debug('IndexedDB not available');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
return false;
}

return new Promise<boolean>((resolve) => {
try {
// For Chrome, we need to be more careful with initialization
if (isChrome()) {
// First, try to open a test database
const testRequest = window.indexedDB.open('test', 1);
testRequest.onerror = () => {
logger.error('Test database failed');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
};

testRequest.onsuccess = () => {
// Close and delete test database
const testDb = testRequest.result;
testDb.close();
const deleteRequest = window.indexedDB.deleteDatabase('test');

deleteRequest.onsuccess = () => {
// Now try to open the actual database
const request = window.indexedDB.open('boltHistory', 1);

request.onerror = () => {
logger.error('Failed to open database');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
};

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
}
};

request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;

// Test if we can actually use the database
try {
const transaction = db.transaction(['chats'], 'readonly');
transaction.oncomplete = () => {
logger.debug('Database test successful');
window.__BOLT_PERSISTENCE_AVAILABLE__ = true;
resolve(true);
};
transaction.onerror = () => {
logger.error('Database test failed');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
};
} catch (error) {
logger.error('Error testing database:', error);
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
}
};
};

deleteRequest.onerror = () => {
logger.error('Failed to delete test database');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
};
};
} else {
// For other browsers, use the standard approach
const request = window.indexedDB.open('boltHistory', 1);
request.onerror = () => {
logger.error('Failed to open database');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
};

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
}
};

request.onsuccess = () => {
logger.debug('Database initialized');
window.__BOLT_PERSISTENCE_AVAILABLE__ = true;
resolve(true);
};
}
} catch (error) {
logger.error('Error initializing database:', error);
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
resolve(false);
}
});
}

// Set initial persistence state
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;

// Initialize IndexedDB before hydrating the app
initIndexedDB().then(() => {
startTransition(() => {
hydrateRoot(document.getElementById('root')!, <RemixBrowser />);
});
});

// Add type declaration
declare global {
interface Window {
__BOLT_PERSISTENCE_AVAILABLE__: boolean;
}
}
106 changes: 61 additions & 45 deletions app/lib/persistence/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,62 +37,78 @@ export async function openDatabase(): Promise<IDBDatabase | undefined> {
return;
}

const request = indexedDB.open('boltHistory', 1);
// Test if we can actually open IndexedDB
const testRequest = window.indexedDB.open('test');
testRequest.onerror = () => {
logger.error('IndexedDB test failed');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
};

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
logger.debug('Upgrading database');
testRequest.onsuccess = () => {
// Close and delete test database
const db = testRequest.result;
db.close();
window.indexedDB.deleteDatabase('test');

if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
logger.debug('Created chats store');
}
};
// Now open the actual database
const request = window.indexedDB.open('boltHistory', 1);

request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
logger.debug('Successfully opened database');

// Test if we can actually use the database
try {
const transaction = db.transaction(['chats'], 'readonly');
transaction.oncomplete = () => {
logger.debug('Database test successful');
dbInitAttempted = true;
dbInitializing = false;
resolve(db);
};
transaction.onerror = () => {
logger.error('Database test failed');
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
logger.debug('Upgrading database');

if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
logger.debug('Created chats store');
}
};

request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
logger.debug('Successfully opened database');

// Test if we can actually use the database
try {
const transaction = db.transaction(['chats'], 'readonly');
transaction.oncomplete = () => {
logger.debug('Database test successful');
dbInitAttempted = true;
dbInitializing = false;
resolve(db);
};
transaction.onerror = () => {
logger.error('Database test failed');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
};
} catch (error) {
logger.error('Error testing database:', error);
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
};
} catch (error) {
logger.error('Error testing database:', error);
}
};

request.onerror = (event: Event) => {
const error = (event.target as IDBOpenDBRequest).error;
logger.error('Failed to open database:', error?.message || 'Unknown error');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
}
};
};

request.onerror = (event: Event) => {
const error = (event.target as IDBOpenDBRequest).error;
logger.error('Failed to open database:', error?.message || 'Unknown error');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
};

request.onblocked = () => {
logger.error('Database blocked');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
request.onblocked = () => {
logger.error('Database blocked');
dbInitAttempted = true;
dbInitializing = false;
resolve(undefined);
};
};

} catch (error) {
logger.error('Error initializing database:', error);
dbInitAttempted = true;
Expand Down
57 changes: 47 additions & 10 deletions app/lib/persistence/useChatHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,23 @@ async function initializeDb() {

dbInitializing = true;
try {
// Check if we're in a browser environment
if (typeof window === 'undefined') {
logger.debug('Not in browser environment');
return undefined;
}

// Check if persistence is available
if (!window.__BOLT_PERSISTENCE_AVAILABLE__) {
logger.debug('Persistence not available');
return undefined;
}

db = await openDatabase();
dbInitialized = true;
logger.debug('Database initialized successfully');
if (db) {
dbInitialized = true;
logger.debug('Database initialized successfully');
}
} catch (error) {
logger.error('Failed to initialize database:', error);
} finally {
Expand All @@ -54,23 +68,39 @@ export function useChatHistory() {
useEffect(() => {
const init = async () => {
try {
// Always try to initialize the database
const database = await initializeDb();

// If we have a mixedId but no database, navigate home silently
if (mixedId && !database) {
navigate('/', { replace: true });
setReady(true);
return;
}

// If we have both mixedId and database, try to load messages
if (mixedId && database) {
const storedMessages = await getMessages(database, mixedId);
if (storedMessages && storedMessages.messages.length > 0) {
setInitialMessages(storedMessages.messages);
setUrlId(storedMessages.urlId);
description.set(storedMessages.description);
chatId.set(storedMessages.id);
} else {
try {
const storedMessages = await getMessages(database, mixedId);
if (storedMessages && storedMessages.messages.length > 0) {
setInitialMessages(storedMessages.messages);
setUrlId(storedMessages.urlId);
description.set(storedMessages.description);
chatId.set(storedMessages.id);
} else {
navigate('/', { replace: true });
}
} catch (error) {
logger.error('Failed to load messages:', error);
navigate('/', { replace: true });
}
}

setReady(true);
} catch (error) {
logger.error('Failed to initialize:', error);
setReady(true);
}
setReady(true);
};

init();
Expand Down Expand Up @@ -119,3 +149,10 @@ function navigateChat(nextId: string) {
url.pathname = `/chat/${nextId}`;
window.history.replaceState({}, '', url);
}

// Add type declaration
declare global {
interface Window {
__BOLT_PERSISTENCE_AVAILABLE__: boolean;
}
}
Loading

0 comments on commit dadf2c1

Please sign in to comment.