Skip to content

Commit

Permalink
feat: improve IndexedDB persistence handling
Browse files Browse the repository at this point in the history
- 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 0810433 commit 9cc2cde
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 35 deletions.
140 changes: 119 additions & 21 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,134 @@
import { RemixBrowser } from '@remix-run/react';
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { createScopedLogger } from '~/utils/logger';

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) {
return new Promise((resolve) => {
const request = window.indexedDB.open('boltHistory', 1);
request.onerror = () => {
console.error('Failed to initialize IndexedDB');
resolve(false);
};
request.onsuccess = () => {
console.debug('IndexedDB initialized');
resolve(true);
};
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 });
}
};
});
if (typeof window === 'undefined' || !window.indexedDB) {
logger.debug('IndexedDB not available');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
return false;
}
return Promise.resolve(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;
}
}
45 changes: 34 additions & 11 deletions app/lib/persistence/useChatHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ async function initializeDb() {
return undefined;
}

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

Expand Down Expand Up @@ -68,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 @@ -133,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;
}
}
85 changes: 82 additions & 3 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,81 @@ const inlineThemeCode = stripIndents`
// Initialize IndexedDB early
if (typeof window !== 'undefined' && window.indexedDB) {
const request = window.indexedDB.open('boltHistory', 1);
request.onerror = () => console.error('Failed to initialize IndexedDB');
request.onsuccess = () => console.debug('IndexedDB initialized');
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
// Check if we're in Chrome
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
if (isChrome) {
// For Chrome, we need to be more careful with initialization
const testRequest = window.indexedDB.open('test', 1);
testRequest.onerror = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = 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.onupgradeneeded = (event) => {
const db = event.target.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.result;
// Test if we can actually use the database
try {
const transaction = db.transaction(['chats'], 'readonly');
transaction.oncomplete = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = true;
};
transaction.onerror = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
};
} catch (error) {
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
}
};
request.onerror = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
};
};
deleteRequest.onerror = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
};
};
} else {
// For other browsers, use the standard approach
const request = window.indexedDB.open('boltHistory', 1);
request.onupgradeneeded = (event) => {
const db = event.target.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 = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = true;
};
request.onerror = () => {
window.__BOLT_PERSISTENCE_AVAILABLE__ = false;
};
}
}
`;

Expand Down Expand Up @@ -88,3 +160,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
export default function App() {
return <Outlet />;
}

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

0 comments on commit 9cc2cde

Please sign in to comment.