documentation-hub 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB wrapper for session persistence
|
|
3
|
+
* Provides a simple interface for storing and retrieving session data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import logger from './logger';
|
|
7
|
+
import { safeJsonParse } from './safeJsonParse';
|
|
8
|
+
import type { Session, Document as SessionDocument } from '@/types/session';
|
|
9
|
+
|
|
10
|
+
const DB_NAME = 'DocHubDB';
|
|
11
|
+
const DB_VERSION = 1;
|
|
12
|
+
const SESSIONS_STORE = 'sessions';
|
|
13
|
+
|
|
14
|
+
interface DBConfig {
|
|
15
|
+
dbName: string;
|
|
16
|
+
version: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Serialized session type for IndexedDB (dates as ISO strings)
|
|
20
|
+
type SerializedDocument = Omit<SessionDocument, 'processedAt'> & {
|
|
21
|
+
processedAt?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SerializedSession = Omit<Session, 'createdAt' | 'lastModified' | 'closedAt' | 'documents'> & {
|
|
25
|
+
createdAt: string;
|
|
26
|
+
lastModified: string;
|
|
27
|
+
closedAt?: string;
|
|
28
|
+
documents: SerializedDocument[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Connection Pool Manager for IndexedDB
|
|
33
|
+
* Maintains a single connection throughout the app lifecycle
|
|
34
|
+
* Provides automatic reconnection on failure
|
|
35
|
+
*/
|
|
36
|
+
class IndexedDBConnectionPool {
|
|
37
|
+
private db: IDBDatabase | null = null;
|
|
38
|
+
private isConnecting = false;
|
|
39
|
+
private connectionPromise: Promise<IDBDatabase> | null = null;
|
|
40
|
+
private lastError: Error | null = null;
|
|
41
|
+
private reconnectAttempts = 0;
|
|
42
|
+
private readonly MAX_RECONNECT_ATTEMPTS = 3;
|
|
43
|
+
private readonly RECONNECT_DELAY = 1000; // 1 second
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get database connection (creates if not exists)
|
|
47
|
+
* Uses singleton pattern to ensure only one connection
|
|
48
|
+
*/
|
|
49
|
+
async getConnection(): Promise<IDBDatabase> {
|
|
50
|
+
// If we have a valid connection, return it
|
|
51
|
+
if (this.db && this.db.objectStoreNames.length > 0) {
|
|
52
|
+
// Check if connection is still valid
|
|
53
|
+
try {
|
|
54
|
+
// Simple health check - access object store names
|
|
55
|
+
const _objectStoreNames = this.db.objectStoreNames;
|
|
56
|
+
return this.db;
|
|
57
|
+
} catch (_error) {
|
|
58
|
+
logger.warn('[IndexedDB Pool] Connection invalid, reconnecting...');
|
|
59
|
+
this.db = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If already connecting, wait for that connection
|
|
64
|
+
if (this.isConnecting && this.connectionPromise) {
|
|
65
|
+
return this.connectionPromise;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create new connection
|
|
69
|
+
this.isConnecting = true;
|
|
70
|
+
this.connectionPromise = this.createConnection();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
this.db = await this.connectionPromise;
|
|
74
|
+
this.reconnectAttempts = 0; // Reset on successful connection
|
|
75
|
+
return this.db;
|
|
76
|
+
} finally {
|
|
77
|
+
this.isConnecting = false;
|
|
78
|
+
this.connectionPromise = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a new database connection
|
|
84
|
+
*/
|
|
85
|
+
private createConnection(): Promise<IDBDatabase> {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
88
|
+
|
|
89
|
+
request.onerror = () => {
|
|
90
|
+
const error = new Error(
|
|
91
|
+
`Failed to open database: ${request.error?.message || 'Unknown error'}`
|
|
92
|
+
);
|
|
93
|
+
this.lastError = error;
|
|
94
|
+
logger.error('[IndexedDB Pool] Connection failed:', error);
|
|
95
|
+
reject(error);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
request.onsuccess = () => {
|
|
99
|
+
const db = request.result;
|
|
100
|
+
logger.info('[IndexedDB Pool] Connection established');
|
|
101
|
+
|
|
102
|
+
// Set up connection error handlers
|
|
103
|
+
db.onerror = (event) => {
|
|
104
|
+
logger.error('[IndexedDB Pool] Database error:', event);
|
|
105
|
+
this.handleConnectionError();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
db.onclose = () => {
|
|
109
|
+
logger.info('[IndexedDB Pool] Connection closed');
|
|
110
|
+
this.db = null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
resolve(db);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
request.onupgradeneeded = (event) => {
|
|
117
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
118
|
+
|
|
119
|
+
// Create sessions object store if it doesn't exist
|
|
120
|
+
if (!db.objectStoreNames.contains(SESSIONS_STORE)) {
|
|
121
|
+
const sessionsStore = db.createObjectStore(SESSIONS_STORE, {
|
|
122
|
+
keyPath: 'id',
|
|
123
|
+
});
|
|
124
|
+
// Create indexes for faster queries
|
|
125
|
+
sessionsStore.createIndex('status', 'status', { unique: false });
|
|
126
|
+
sessionsStore.createIndex('lastModified', 'lastModified', { unique: false });
|
|
127
|
+
sessionsStore.createIndex('createdAt', 'createdAt', { unique: false });
|
|
128
|
+
|
|
129
|
+
logger.info('[IndexedDB Pool] Database upgraded to version', DB_VERSION);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
request.onblocked = () => {
|
|
134
|
+
logger.warn('[IndexedDB Pool] Database upgrade blocked by other tabs');
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handle connection errors with automatic retry
|
|
141
|
+
*/
|
|
142
|
+
private async handleConnectionError(): Promise<void> {
|
|
143
|
+
this.db = null;
|
|
144
|
+
|
|
145
|
+
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
|
|
146
|
+
this.reconnectAttempts++;
|
|
147
|
+
logger.info(
|
|
148
|
+
`[IndexedDB Pool] Attempting reconnection (${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Wait before reconnecting
|
|
152
|
+
await new Promise((resolve) =>
|
|
153
|
+
setTimeout(resolve, this.RECONNECT_DELAY * this.reconnectAttempts)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await this.getConnection();
|
|
158
|
+
logger.info('[IndexedDB Pool] Reconnection successful');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error('[IndexedDB Pool] Reconnection failed:', error);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
logger.error('[IndexedDB Pool] Max reconnection attempts reached');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Close the database connection (for cleanup)
|
|
169
|
+
*/
|
|
170
|
+
close(): void {
|
|
171
|
+
if (this.db) {
|
|
172
|
+
logger.info('[IndexedDB Pool] Closing connection');
|
|
173
|
+
this.db.close();
|
|
174
|
+
this.db = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get connection statistics
|
|
180
|
+
*/
|
|
181
|
+
getStats(): {
|
|
182
|
+
connected: boolean;
|
|
183
|
+
reconnectAttempts: number;
|
|
184
|
+
lastError: Error | null;
|
|
185
|
+
} {
|
|
186
|
+
return {
|
|
187
|
+
connected: this.db !== null,
|
|
188
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
189
|
+
lastError: this.lastError,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create singleton instance
|
|
195
|
+
const connectionPool = new IndexedDBConnectionPool();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* GlobalStats Connection Pool Manager for IndexedDB
|
|
199
|
+
* Maintains a single connection for GlobalStats database
|
|
200
|
+
* Separate from main connection pool to avoid cross-database issues
|
|
201
|
+
*/
|
|
202
|
+
class GlobalStatsConnectionPool {
|
|
203
|
+
private db: IDBDatabase | null = null;
|
|
204
|
+
private isConnecting = false;
|
|
205
|
+
private connectionPromise: Promise<IDBDatabase> | null = null;
|
|
206
|
+
private lastError: Error | null = null;
|
|
207
|
+
private reconnectAttempts = 0;
|
|
208
|
+
private readonly MAX_RECONNECT_ATTEMPTS = 3;
|
|
209
|
+
private readonly RECONNECT_DELAY = 1000; // 1 second
|
|
210
|
+
|
|
211
|
+
private readonly DB_NAME = 'DocHub_GlobalStats';
|
|
212
|
+
private readonly DB_VERSION = 1;
|
|
213
|
+
private readonly STATS_STORE = 'stats';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get database connection (creates if not exists)
|
|
217
|
+
* Uses singleton pattern to ensure only one connection
|
|
218
|
+
*/
|
|
219
|
+
async getConnection(): Promise<IDBDatabase> {
|
|
220
|
+
// If we have a valid connection, return it
|
|
221
|
+
if (this.db && this.db.objectStoreNames.length > 0) {
|
|
222
|
+
// Check if connection is still valid
|
|
223
|
+
try {
|
|
224
|
+
// Simple health check - access object store names
|
|
225
|
+
const _objectStoreNames = this.db.objectStoreNames;
|
|
226
|
+
return this.db;
|
|
227
|
+
} catch (_error) {
|
|
228
|
+
logger.warn('[GlobalStats Pool] Connection invalid, reconnecting...');
|
|
229
|
+
this.db = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If already connecting, wait for that connection
|
|
234
|
+
if (this.isConnecting && this.connectionPromise) {
|
|
235
|
+
return this.connectionPromise;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Create new connection
|
|
239
|
+
this.isConnecting = true;
|
|
240
|
+
this.connectionPromise = this.createConnection();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
this.db = await this.connectionPromise;
|
|
244
|
+
this.reconnectAttempts = 0; // Reset on successful connection
|
|
245
|
+
return this.db;
|
|
246
|
+
} finally {
|
|
247
|
+
this.isConnecting = false;
|
|
248
|
+
this.connectionPromise = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create a new database connection
|
|
254
|
+
*/
|
|
255
|
+
private createConnection(): Promise<IDBDatabase> {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
|
258
|
+
|
|
259
|
+
request.onerror = () => {
|
|
260
|
+
const error = new Error(
|
|
261
|
+
`Failed to open GlobalStats database: ${request.error?.message || 'Unknown error'}`
|
|
262
|
+
);
|
|
263
|
+
this.lastError = error;
|
|
264
|
+
logger.error('[GlobalStats Pool] Connection failed:', error);
|
|
265
|
+
reject(error);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
request.onsuccess = () => {
|
|
269
|
+
const db = request.result;
|
|
270
|
+
logger.info('[GlobalStats Pool] Connection established');
|
|
271
|
+
|
|
272
|
+
// Set up connection error handlers
|
|
273
|
+
db.onerror = (event) => {
|
|
274
|
+
logger.error('[GlobalStats Pool] Database error:', event);
|
|
275
|
+
this.handleConnectionError();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
db.onclose = () => {
|
|
279
|
+
logger.info('[GlobalStats Pool] Connection closed');
|
|
280
|
+
this.db = null;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
resolve(db);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
request.onupgradeneeded = (event) => {
|
|
287
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
288
|
+
|
|
289
|
+
// Create stats object store if it doesn't exist
|
|
290
|
+
if (!db.objectStoreNames.contains(this.STATS_STORE)) {
|
|
291
|
+
db.createObjectStore(this.STATS_STORE);
|
|
292
|
+
logger.info('[GlobalStats Pool] Database upgraded to version', this.DB_VERSION);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
request.onblocked = () => {
|
|
297
|
+
logger.warn('[GlobalStats Pool] Database upgrade blocked by other tabs');
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Handle connection errors with automatic retry
|
|
304
|
+
*/
|
|
305
|
+
private async handleConnectionError(): Promise<void> {
|
|
306
|
+
this.db = null;
|
|
307
|
+
|
|
308
|
+
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
|
|
309
|
+
this.reconnectAttempts++;
|
|
310
|
+
logger.info(
|
|
311
|
+
`[GlobalStats Pool] Attempting reconnection (${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Wait before reconnecting
|
|
315
|
+
await new Promise((resolve) =>
|
|
316
|
+
setTimeout(resolve, this.RECONNECT_DELAY * this.reconnectAttempts)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await this.getConnection();
|
|
321
|
+
logger.info('[GlobalStats Pool] Reconnection successful');
|
|
322
|
+
} catch (error) {
|
|
323
|
+
logger.error('[GlobalStats Pool] Reconnection failed:', error);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
logger.error('[GlobalStats Pool] Max reconnection attempts reached');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Close the database connection (for cleanup)
|
|
332
|
+
*/
|
|
333
|
+
close(): void {
|
|
334
|
+
if (this.db) {
|
|
335
|
+
logger.info('[GlobalStats Pool] Closing connection');
|
|
336
|
+
this.db.close();
|
|
337
|
+
this.db = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get connection statistics
|
|
343
|
+
*/
|
|
344
|
+
getStats(): {
|
|
345
|
+
connected: boolean;
|
|
346
|
+
reconnectAttempts: number;
|
|
347
|
+
lastError: Error | null;
|
|
348
|
+
} {
|
|
349
|
+
return {
|
|
350
|
+
connected: this.db !== null,
|
|
351
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
352
|
+
lastError: this.lastError,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create singleton instance for GlobalStats
|
|
358
|
+
const globalStatsConnectionPool = new GlobalStatsConnectionPool();
|
|
359
|
+
|
|
360
|
+
// Close connections on window unload
|
|
361
|
+
if (typeof window !== 'undefined') {
|
|
362
|
+
window.addEventListener('beforeunload', () => {
|
|
363
|
+
connectionPool.close();
|
|
364
|
+
globalStatsConnectionPool.close();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get pooled database connection
|
|
370
|
+
* @deprecated Use connectionPool.getConnection() instead
|
|
371
|
+
*/
|
|
372
|
+
async function openDB(): Promise<IDBDatabase> {
|
|
373
|
+
return connectionPool.getConnection();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Save a session to IndexedDB with quota error recovery
|
|
378
|
+
* Uses connection pool for better performance
|
|
379
|
+
* Handles QuotaExceededError by triggering cleanup
|
|
380
|
+
*/
|
|
381
|
+
export async function saveSession(session: SerializedSession): Promise<void> {
|
|
382
|
+
const db = await connectionPool.getConnection();
|
|
383
|
+
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readwrite');
|
|
386
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
387
|
+
const request = store.put(session);
|
|
388
|
+
|
|
389
|
+
request.onsuccess = () => {
|
|
390
|
+
resolve();
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
request.onerror = () => {
|
|
394
|
+
const error = request.error;
|
|
395
|
+
// Check if this is a quota exceeded error
|
|
396
|
+
if (error?.name === 'QuotaExceededError') {
|
|
397
|
+
logger.error(`[IndexedDB] Quota exceeded for session: ${session.id}`);
|
|
398
|
+
reject(new Error('DATABASE_QUOTA_EXCEEDED'));
|
|
399
|
+
} else {
|
|
400
|
+
reject(new Error(`Failed to save session: ${session.id}`));
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
transaction.onerror = () => {
|
|
405
|
+
const error = transaction.error;
|
|
406
|
+
if (error?.name === 'QuotaExceededError') {
|
|
407
|
+
logger.error(`[IndexedDB] Transaction quota exceeded for session: ${session.id}`);
|
|
408
|
+
reject(new Error('DATABASE_QUOTA_EXCEEDED'));
|
|
409
|
+
} else {
|
|
410
|
+
reject(new Error(`Transaction failed for session: ${session.id}`));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Load all sessions from IndexedDB
|
|
418
|
+
* Uses connection pool for better performance
|
|
419
|
+
*/
|
|
420
|
+
export async function loadSessions(): Promise<SerializedSession[]> {
|
|
421
|
+
const db = await connectionPool.getConnection();
|
|
422
|
+
|
|
423
|
+
return new Promise((resolve, reject) => {
|
|
424
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readonly');
|
|
425
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
426
|
+
const request = store.getAll();
|
|
427
|
+
|
|
428
|
+
request.onsuccess = () => {
|
|
429
|
+
resolve(request.result || []);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
request.onerror = () => {
|
|
433
|
+
reject(new Error('Failed to load sessions'));
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Load a single session by ID from IndexedDB
|
|
440
|
+
* Uses connection pool for better performance
|
|
441
|
+
*/
|
|
442
|
+
export async function loadSessionById(sessionId: string): Promise<SerializedSession | null> {
|
|
443
|
+
const db = await connectionPool.getConnection();
|
|
444
|
+
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readonly');
|
|
447
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
448
|
+
const request = store.get(sessionId);
|
|
449
|
+
|
|
450
|
+
request.onsuccess = () => {
|
|
451
|
+
resolve(request.result || null);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
request.onerror = () => {
|
|
455
|
+
reject(new Error(`Failed to load session: ${sessionId}`));
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Delete a session from IndexedDB
|
|
462
|
+
* Uses connection pool for better performance
|
|
463
|
+
*/
|
|
464
|
+
export async function deleteSession(sessionId: string): Promise<void> {
|
|
465
|
+
const db = await connectionPool.getConnection();
|
|
466
|
+
|
|
467
|
+
return new Promise((resolve, reject) => {
|
|
468
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readwrite');
|
|
469
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
470
|
+
const request = store.delete(sessionId);
|
|
471
|
+
|
|
472
|
+
request.onsuccess = () => {
|
|
473
|
+
resolve();
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
request.onerror = () => {
|
|
477
|
+
reject(new Error(`Failed to delete session: ${sessionId}`));
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Delete all sessions from IndexedDB
|
|
484
|
+
* Uses connection pool for better performance
|
|
485
|
+
*/
|
|
486
|
+
export async function clearAllSessions(): Promise<void> {
|
|
487
|
+
const db = await connectionPool.getConnection();
|
|
488
|
+
|
|
489
|
+
return new Promise((resolve, reject) => {
|
|
490
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readwrite');
|
|
491
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
492
|
+
const request = store.clear();
|
|
493
|
+
|
|
494
|
+
request.onsuccess = () => {
|
|
495
|
+
resolve();
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
request.onerror = () => {
|
|
499
|
+
reject(new Error('Failed to clear sessions'));
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Migrate data from localStorage to IndexedDB
|
|
506
|
+
* This is a one-time migration helper
|
|
507
|
+
*/
|
|
508
|
+
export async function migrateFromLocalStorage(): Promise<void> {
|
|
509
|
+
try {
|
|
510
|
+
const storedSessions = localStorage.getItem('sessions');
|
|
511
|
+
|
|
512
|
+
if (!storedSessions) {
|
|
513
|
+
logger.debug('[IndexedDB] No sessions found in localStorage to migrate');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const sessions = safeJsonParse<SerializedSession[]>(
|
|
518
|
+
storedSessions,
|
|
519
|
+
[],
|
|
520
|
+
'localStorage migration'
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
524
|
+
logger.debug('[IndexedDB] No valid sessions to migrate');
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
logger.info(`[IndexedDB] Migrating ${sessions.length} session(s) from localStorage...`);
|
|
529
|
+
|
|
530
|
+
// Save all sessions to IndexedDB
|
|
531
|
+
for (const session of sessions) {
|
|
532
|
+
await saveSession(session);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
logger.info('[IndexedDB] Migration completed successfully');
|
|
536
|
+
|
|
537
|
+
// Optionally remove from localStorage after successful migration
|
|
538
|
+
// Uncomment the following line if you want to remove old data
|
|
539
|
+
// localStorage.removeItem('sessions');
|
|
540
|
+
} catch (error) {
|
|
541
|
+
logger.error('[IndexedDB] Migration failed:', error);
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get active session IDs from IndexedDB
|
|
548
|
+
* Uses connection pool for better performance
|
|
549
|
+
*/
|
|
550
|
+
export async function getActiveSessionIds(): Promise<string[]> {
|
|
551
|
+
const db = await connectionPool.getConnection();
|
|
552
|
+
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readonly');
|
|
555
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
556
|
+
const index = store.index('status');
|
|
557
|
+
const request = index.getAllKeys('active');
|
|
558
|
+
|
|
559
|
+
request.onsuccess = () => {
|
|
560
|
+
resolve(request.result as string[]);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
request.onerror = () => {
|
|
564
|
+
reject(new Error('Failed to get active session IDs'));
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Calculate approximate database size in MB
|
|
571
|
+
* Uses JSON string length as proxy for storage size
|
|
572
|
+
*/
|
|
573
|
+
export async function calculateDBSize(): Promise<number> {
|
|
574
|
+
try {
|
|
575
|
+
const sessions = await loadSessions();
|
|
576
|
+
const jsonString = JSON.stringify(sessions);
|
|
577
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
578
|
+
const sizeInMB = sizeInBytes / (1024 * 1024);
|
|
579
|
+
|
|
580
|
+
logger.debug(
|
|
581
|
+
`[IndexedDB] Database size: ${sizeInMB.toFixed(2)}MB (${sessions.length} sessions)`
|
|
582
|
+
);
|
|
583
|
+
return sizeInMB;
|
|
584
|
+
} catch (error) {
|
|
585
|
+
logger.error('[IndexedDB] Failed to calculate database size:', error);
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Get oldest closed sessions sorted by closedAt date
|
|
592
|
+
* Uses connection pool for better performance
|
|
593
|
+
*/
|
|
594
|
+
export async function getOldestClosedSessions(limit: number): Promise<SerializedSession[]> {
|
|
595
|
+
const db = await connectionPool.getConnection();
|
|
596
|
+
|
|
597
|
+
return new Promise((resolve, reject) => {
|
|
598
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readonly');
|
|
599
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
600
|
+
const request = store.getAll();
|
|
601
|
+
|
|
602
|
+
request.onsuccess = () => {
|
|
603
|
+
const sessions = request.result || [];
|
|
604
|
+
|
|
605
|
+
// Filter closed sessions and sort by closedAt (oldest first)
|
|
606
|
+
const closedSessions = sessions
|
|
607
|
+
.filter((s: SerializedSession) => s.status === 'closed' && s.closedAt)
|
|
608
|
+
.sort((a: SerializedSession, b: SerializedSession) => {
|
|
609
|
+
// closedAt is guaranteed to exist from the filter above
|
|
610
|
+
const dateA = new Date(a.closedAt!).getTime();
|
|
611
|
+
const dateB = new Date(b.closedAt!).getTime();
|
|
612
|
+
return dateA - dateB; // Oldest first
|
|
613
|
+
})
|
|
614
|
+
.slice(0, limit);
|
|
615
|
+
|
|
616
|
+
resolve(closedSessions);
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
request.onerror = () => {
|
|
620
|
+
reject(new Error('Failed to get oldest closed sessions'));
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Delete multiple sessions by their IDs
|
|
627
|
+
* Uses connection pool for better performance
|
|
628
|
+
*/
|
|
629
|
+
export async function deleteSessions(sessionIds: string[]): Promise<number> {
|
|
630
|
+
const db = await connectionPool.getConnection();
|
|
631
|
+
let deletedCount = 0;
|
|
632
|
+
|
|
633
|
+
return new Promise((resolve, reject) => {
|
|
634
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readwrite');
|
|
635
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
636
|
+
|
|
637
|
+
for (const sessionId of sessionIds) {
|
|
638
|
+
const request = store.delete(sessionId);
|
|
639
|
+
request.onsuccess = () => {
|
|
640
|
+
deletedCount++;
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
transaction.oncomplete = () => {
|
|
645
|
+
logger.info(`[IndexedDB] Deleted ${deletedCount} session(s)`);
|
|
646
|
+
resolve(deletedCount);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
transaction.onerror = () => {
|
|
650
|
+
reject(new Error('Failed to delete sessions'));
|
|
651
|
+
};
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Clean up database when it exceeds size limit
|
|
657
|
+
* Removes oldest closed sessions until under the limit
|
|
658
|
+
*/
|
|
659
|
+
export async function ensureDBSizeLimit(maxSizeMB: number = 200): Promise<void> {
|
|
660
|
+
try {
|
|
661
|
+
const currentSize = await calculateDBSize();
|
|
662
|
+
|
|
663
|
+
if (currentSize <= maxSizeMB) {
|
|
664
|
+
return; // Within limits
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
logger.warn(
|
|
668
|
+
`[IndexedDB] Database size (${currentSize.toFixed(2)}MB) exceeds limit (${maxSizeMB}MB)`
|
|
669
|
+
);
|
|
670
|
+
logger.info('[IndexedDB] Starting cleanup of oldest closed sessions...');
|
|
671
|
+
|
|
672
|
+
// Delete oldest closed sessions in batches until under limit
|
|
673
|
+
let iterationCount = 0;
|
|
674
|
+
const maxIterations = 10; // Safety limit
|
|
675
|
+
|
|
676
|
+
while (iterationCount < maxIterations) {
|
|
677
|
+
const oldestSessions = await getOldestClosedSessions(10); // Delete 10 at a time
|
|
678
|
+
|
|
679
|
+
if (oldestSessions.length === 0) {
|
|
680
|
+
logger.debug('[IndexedDB] No more closed sessions to delete');
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const sessionIds = oldestSessions.map((s: SerializedSession) => s.id);
|
|
685
|
+
await deleteSessions(sessionIds);
|
|
686
|
+
|
|
687
|
+
const newSize = await calculateDBSize();
|
|
688
|
+
logger.debug(`[IndexedDB] Size after cleanup: ${newSize.toFixed(2)}MB`);
|
|
689
|
+
|
|
690
|
+
if (newSize <= maxSizeMB) {
|
|
691
|
+
logger.info('[IndexedDB] Database size now under limit');
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
iterationCount++;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (iterationCount >= maxIterations) {
|
|
699
|
+
logger.warn('[IndexedDB] Max cleanup iterations reached, size may still exceed limit');
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
logger.error('[IndexedDB] Failed to ensure database size limit:', error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Truncate large change arrays in session documents
|
|
708
|
+
* Prevents excessive storage of tracking data
|
|
709
|
+
*/
|
|
710
|
+
export function truncateSessionChanges(
|
|
711
|
+
session: SerializedSession,
|
|
712
|
+
maxChanges: number = 100
|
|
713
|
+
): SerializedSession {
|
|
714
|
+
if (!session.documents) {
|
|
715
|
+
return session;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
...session,
|
|
720
|
+
documents: session.documents.map((doc: SerializedDocument) => {
|
|
721
|
+
if (doc.processingResult?.changes && doc.processingResult.changes.length > maxChanges) {
|
|
722
|
+
return {
|
|
723
|
+
...doc,
|
|
724
|
+
processingResult: {
|
|
725
|
+
...doc.processingResult,
|
|
726
|
+
changes: doc.processingResult.changes.slice(0, maxChanges),
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return doc;
|
|
731
|
+
}),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Export connection pool for advanced use cases
|
|
737
|
+
* Provides direct access to the pooled connection
|
|
738
|
+
*/
|
|
739
|
+
export const getConnectionPool = () => connectionPool;
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Get database performance statistics
|
|
743
|
+
*/
|
|
744
|
+
export async function getDBPerformanceStats(): Promise<{
|
|
745
|
+
connected: boolean;
|
|
746
|
+
reconnectAttempts: number;
|
|
747
|
+
lastError: Error | null;
|
|
748
|
+
sessionCount?: number;
|
|
749
|
+
estimatedSizeMB?: number;
|
|
750
|
+
}> {
|
|
751
|
+
const poolStats = connectionPool.getStats();
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const sessions = await loadSessions();
|
|
755
|
+
const sizeMB = await calculateDBSize();
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
...poolStats,
|
|
759
|
+
sessionCount: sessions.length,
|
|
760
|
+
estimatedSizeMB: sizeMB,
|
|
761
|
+
};
|
|
762
|
+
} catch (error) {
|
|
763
|
+
return {
|
|
764
|
+
...poolStats,
|
|
765
|
+
sessionCount: 0,
|
|
766
|
+
estimatedSizeMB: 0,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Handle quota exceeded errors with automatic cleanup and retry
|
|
773
|
+
* Attempts to free up space and retry the operation
|
|
774
|
+
*/
|
|
775
|
+
export async function handleQuotaExceededError(
|
|
776
|
+
operation: () => Promise<void>,
|
|
777
|
+
sessionId: string,
|
|
778
|
+
maxRetries: number = 2
|
|
779
|
+
): Promise<void> {
|
|
780
|
+
let retries = 0;
|
|
781
|
+
|
|
782
|
+
while (retries <= maxRetries) {
|
|
783
|
+
try {
|
|
784
|
+
await operation();
|
|
785
|
+
return; // Success
|
|
786
|
+
} catch (error) {
|
|
787
|
+
if (error instanceof Error && error.message === 'DATABASE_QUOTA_EXCEEDED') {
|
|
788
|
+
if (retries < maxRetries) {
|
|
789
|
+
logger.warn(
|
|
790
|
+
`[IndexedDB] Quota exceeded, attempting cleanup (attempt ${retries + 1}/${maxRetries})`
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// Aggressive cleanup - delete oldest closed sessions
|
|
794
|
+
const oldestSessions = await getOldestClosedSessions(20);
|
|
795
|
+
if (oldestSessions.length > 0) {
|
|
796
|
+
const sessionIds = oldestSessions.map((s: SerializedSession) => s.id);
|
|
797
|
+
await deleteSessions(sessionIds);
|
|
798
|
+
logger.info(`[IndexedDB] Deleted ${oldestSessions.length} session(s) to free up space`);
|
|
799
|
+
} else {
|
|
800
|
+
// No more closed sessions, truncate active sessions' change history
|
|
801
|
+
const sessions = await loadSessions();
|
|
802
|
+
const activeSessions = sessions.filter((s: SerializedSession) => s.status === 'active');
|
|
803
|
+
|
|
804
|
+
for (const session of activeSessions.slice(0, 5)) {
|
|
805
|
+
const truncated = truncateSessionChanges(session, 50);
|
|
806
|
+
await saveSession(truncated);
|
|
807
|
+
}
|
|
808
|
+
logger.info('[IndexedDB] Truncated change history in active sessions');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
retries++;
|
|
812
|
+
} else {
|
|
813
|
+
// Max retries exceeded, throw error with guidance
|
|
814
|
+
const sizeMB = await calculateDBSize();
|
|
815
|
+
const error = new Error(
|
|
816
|
+
`DATABASE_QUOTA_EXCEEDED_PERMANENTLY: Database is ${sizeMB.toFixed(2)}MB. ` +
|
|
817
|
+
`Please archive old sessions or export data to free up space.`
|
|
818
|
+
);
|
|
819
|
+
logger.error('[IndexedDB] Permanent quota exceeded:', error);
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
// Not a quota error, re-throw
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* ========================================
|
|
832
|
+
* GlobalStats Database Helper Functions
|
|
833
|
+
* ========================================
|
|
834
|
+
* These functions provide a simple interface for GlobalStats persistence
|
|
835
|
+
* Uses the globalStatsConnectionPool for consistent connection management
|
|
836
|
+
*/
|
|
837
|
+
|
|
838
|
+
// Import GlobalStats types
|
|
839
|
+
import type { GlobalStats } from '@/types/globalStats';
|
|
840
|
+
|
|
841
|
+
const STATS_STORE = 'stats';
|
|
842
|
+
const STATS_KEY = 'global';
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Load global statistics from IndexedDB
|
|
846
|
+
* Uses connection pool for better performance and reliability
|
|
847
|
+
*/
|
|
848
|
+
export async function loadGlobalStats(): Promise<GlobalStats | null> {
|
|
849
|
+
try {
|
|
850
|
+
const db = await globalStatsConnectionPool.getConnection();
|
|
851
|
+
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
const transaction = db.transaction([STATS_STORE], 'readonly');
|
|
854
|
+
const store = transaction.objectStore(STATS_STORE);
|
|
855
|
+
const request = store.get(STATS_KEY);
|
|
856
|
+
|
|
857
|
+
request.onsuccess = () => {
|
|
858
|
+
resolve(request.result || null);
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
request.onerror = () => {
|
|
862
|
+
reject(new Error('Failed to load global stats'));
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
} catch (error) {
|
|
866
|
+
logger.error('[GlobalStats] Failed to load stats:', error);
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Save global statistics to IndexedDB
|
|
873
|
+
* Uses connection pool for better performance and reliability
|
|
874
|
+
*/
|
|
875
|
+
export async function saveGlobalStats(stats: GlobalStats): Promise<void> {
|
|
876
|
+
try {
|
|
877
|
+
const db = await globalStatsConnectionPool.getConnection();
|
|
878
|
+
|
|
879
|
+
return new Promise((resolve, reject) => {
|
|
880
|
+
const transaction = db.transaction([STATS_STORE], 'readwrite');
|
|
881
|
+
const store = transaction.objectStore(STATS_STORE);
|
|
882
|
+
const request = store.put(stats, STATS_KEY);
|
|
883
|
+
|
|
884
|
+
request.onsuccess = () => {
|
|
885
|
+
resolve();
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
request.onerror = () => {
|
|
889
|
+
reject(new Error('Failed to save global stats'));
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
} catch (error) {
|
|
893
|
+
logger.error('[GlobalStats] Failed to save stats:', error);
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Reset global statistics to default values
|
|
900
|
+
* Uses connection pool for better performance and reliability
|
|
901
|
+
*/
|
|
902
|
+
export async function resetGlobalStats(freshStats: GlobalStats): Promise<void> {
|
|
903
|
+
try {
|
|
904
|
+
await saveGlobalStats(freshStats);
|
|
905
|
+
logger.info('[GlobalStats] Stats reset to default values');
|
|
906
|
+
} catch (error) {
|
|
907
|
+
logger.error('[GlobalStats] Failed to reset stats:', error);
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Export the global stats connection pool for advanced use cases
|
|
914
|
+
*/
|
|
915
|
+
export const getGlobalStatsConnectionPool = () => globalStatsConnectionPool;
|