Files
Stirling-PDF/frontend/src/services/indexedDBManager.ts
Reece Browne 949ffa01ad Feature/v2/file handling improvements (#4222)
# Description of Changes

A new universal file context rather than the splintered ones for the
main views, tools and manager we had before (manager still has its own
but its better integreated with the core context)
File context has been split it into a handful of different files
managing various file related issues separately to reduce the monolith -
FileReducer.ts - State management
  fileActions.ts - File operations
  fileSelectors.ts - Data access patterns
  lifecycle.ts - Resource cleanup and memory management
  fileHooks.ts - React hooks interface
  contexts.ts - Context providers
Improved thumbnail generation
Improved indexxedb handling
Stopped handling files as blobs were not necessary to improve
performance
A new library handling drag and drop
https://github.com/atlassian/pragmatic-drag-and-drop (Out of scope yes
but I broke the old one with the new filecontext and it needed doing so
it was a might as well)
A new library handling virtualisation on page editor
@tanstack/react-virtual, as above.
Quickly ripped out the last remnants of the old URL params stuff and
replaced with the beginnings of what will later become the new URL
navigation system (for now it just restores the tool name in url
behavior)
Fixed selected file not regestered when opening a tool
Fixed png thumbnails
Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Reece Browne <you@example.com>
2025-08-21 17:30:26 +01:00

227 lines
6.0 KiB
TypeScript

/**
* Centralized IndexedDB Manager
* Handles all database initialization, schema management, and migrations
* Prevents race conditions and duplicate schema upgrades
*/
export interface DatabaseConfig {
name: string;
version: number;
stores: {
name: string;
keyPath?: string | string[];
autoIncrement?: boolean;
indexes?: {
name: string;
keyPath: string | string[];
unique: boolean;
}[];
}[];
}
class IndexedDBManager {
private static instance: IndexedDBManager;
private databases = new Map<string, IDBDatabase>();
private initPromises = new Map<string, Promise<IDBDatabase>>();
private constructor() {}
static getInstance(): IndexedDBManager {
if (!IndexedDBManager.instance) {
IndexedDBManager.instance = new IndexedDBManager();
}
return IndexedDBManager.instance;
}
/**
* Open or get existing database connection
*/
async openDatabase(config: DatabaseConfig): Promise<IDBDatabase> {
const existingDb = this.databases.get(config.name);
if (existingDb) {
return existingDb;
}
const existingPromise = this.initPromises.get(config.name);
if (existingPromise) {
return existingPromise;
}
const initPromise = this.performDatabaseInit(config);
this.initPromises.set(config.name, initPromise);
try {
const db = await initPromise;
this.databases.set(config.name, db);
return db;
} catch (error) {
this.initPromises.delete(config.name);
throw error;
}
}
private performDatabaseInit(config: DatabaseConfig): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
console.log(`Opening IndexedDB: ${config.name} v${config.version}`);
const request = indexedDB.open(config.name, config.version);
request.onerror = () => {
console.error(`Failed to open ${config.name}:`, request.error);
reject(request.error);
};
request.onsuccess = () => {
const db = request.result;
console.log(`Successfully opened ${config.name}`);
// Set up close handler to clean up our references
db.onclose = () => {
console.log(`Database ${config.name} closed`);
this.databases.delete(config.name);
this.initPromises.delete(config.name);
};
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = request.result;
const oldVersion = event.oldVersion;
console.log(`Upgrading ${config.name} from v${oldVersion} to v${config.version}`);
// Create or update object stores
config.stores.forEach(storeConfig => {
let store: IDBObjectStore;
if (db.objectStoreNames.contains(storeConfig.name)) {
// Store exists - for now, just continue (could add migration logic here)
console.log(`Object store '${storeConfig.name}' already exists`);
return;
}
// Create new object store
const options: IDBObjectStoreParameters = {};
if (storeConfig.keyPath) {
options.keyPath = storeConfig.keyPath;
}
if (storeConfig.autoIncrement) {
options.autoIncrement = storeConfig.autoIncrement;
}
store = db.createObjectStore(storeConfig.name, options);
console.log(`Created object store '${storeConfig.name}'`);
// Create indexes
if (storeConfig.indexes) {
storeConfig.indexes.forEach(indexConfig => {
store.createIndex(
indexConfig.name,
indexConfig.keyPath,
{ unique: indexConfig.unique }
);
console.log(`Created index '${indexConfig.name}' on '${storeConfig.name}'`);
});
}
});
};
});
}
/**
* Get database connection (must be already opened)
*/
getDatabase(name: string): IDBDatabase | null {
return this.databases.get(name) || null;
}
/**
* Close database connection
*/
closeDatabase(name: string): void {
const db = this.databases.get(name);
if (db) {
db.close();
this.databases.delete(name);
this.initPromises.delete(name);
}
}
/**
* Close all database connections
*/
closeAllDatabases(): void {
this.databases.forEach((db, name) => {
console.log(`Closing database: ${name}`);
db.close();
});
this.databases.clear();
this.initPromises.clear();
}
/**
* Delete database completely
*/
async deleteDatabase(name: string): Promise<void> {
// Close connection if open
this.closeDatabase(name);
return new Promise((resolve, reject) => {
const deleteRequest = indexedDB.deleteDatabase(name);
deleteRequest.onerror = () => reject(deleteRequest.error);
deleteRequest.onsuccess = () => {
console.log(`Deleted database: ${name}`);
resolve();
};
});
}
/**
* Check if a database exists and what version it is
*/
async getDatabaseVersion(name: string): Promise<number | null> {
return new Promise((resolve) => {
const request = indexedDB.open(name);
request.onsuccess = () => {
const db = request.result;
const version = db.version;
db.close();
resolve(version);
};
request.onerror = () => resolve(null);
request.onupgradeneeded = () => {
// Cancel the upgrade
request.transaction?.abort();
resolve(null);
};
});
}
}
// Pre-defined database configurations
export const DATABASE_CONFIGS = {
FILES: {
name: 'stirling-pdf-files',
version: 2,
stores: [{
name: 'files',
keyPath: 'id',
indexes: [
{ name: 'name', keyPath: 'name', unique: false },
{ name: 'lastModified', keyPath: 'lastModified', unique: false }
]
}]
} as DatabaseConfig,
DRAFTS: {
name: 'stirling-pdf-drafts',
version: 1,
stores: [{
name: 'drafts',
keyPath: 'id'
}]
} as DatabaseConfig
} as const;
export const indexedDBManager = IndexedDBManager.getInstance();