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,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentSnapshotService - Manages pre-processing document snapshots in IndexedDB
|
|
3
|
+
*
|
|
4
|
+
* Captures document state before processing to enable:
|
|
5
|
+
* - Side-by-side pre vs post processing comparison
|
|
6
|
+
* - Restore/revert functionality
|
|
7
|
+
* - Change visualization
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import logger from '@/utils/logger';
|
|
11
|
+
import type {
|
|
12
|
+
DocumentSnapshot,
|
|
13
|
+
SerializedDocumentSnapshot,
|
|
14
|
+
HyperlinkSnapshot,
|
|
15
|
+
} from '@/types/editor';
|
|
16
|
+
|
|
17
|
+
const DB_NAME = 'DocHub_Snapshots';
|
|
18
|
+
const DB_VERSION = 1;
|
|
19
|
+
const SNAPSHOTS_STORE = 'documentSnapshots';
|
|
20
|
+
|
|
21
|
+
// Maximum age for snapshots (7 days in milliseconds)
|
|
22
|
+
const MAX_SNAPSHOT_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
// Maximum total storage for snapshots (100MB)
|
|
25
|
+
const MAX_STORAGE_BYTES = 100 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Connection pool for snapshot database
|
|
29
|
+
* Maintains singleton connection
|
|
30
|
+
*/
|
|
31
|
+
class SnapshotConnectionPool {
|
|
32
|
+
private db: IDBDatabase | null = null;
|
|
33
|
+
private isConnecting = false;
|
|
34
|
+
private connectionPromise: Promise<IDBDatabase> | null = null;
|
|
35
|
+
|
|
36
|
+
async getConnection(): Promise<IDBDatabase> {
|
|
37
|
+
if (this.db) {
|
|
38
|
+
try {
|
|
39
|
+
// Health check
|
|
40
|
+
const _ = this.db.objectStoreNames;
|
|
41
|
+
return this.db;
|
|
42
|
+
} catch {
|
|
43
|
+
this.db = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this.isConnecting && this.connectionPromise) {
|
|
48
|
+
return this.connectionPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.isConnecting = true;
|
|
52
|
+
this.connectionPromise = this.createConnection();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
this.db = await this.connectionPromise;
|
|
56
|
+
return this.db;
|
|
57
|
+
} finally {
|
|
58
|
+
this.isConnecting = false;
|
|
59
|
+
this.connectionPromise = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private createConnection(): Promise<IDBDatabase> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
66
|
+
|
|
67
|
+
request.onerror = () => {
|
|
68
|
+
logger.error('[SnapshotDB] Connection failed:', request.error);
|
|
69
|
+
reject(new Error(`Failed to open snapshot database: ${request.error?.message}`));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
request.onsuccess = () => {
|
|
73
|
+
const db = request.result;
|
|
74
|
+
logger.info('[SnapshotDB] Connection established');
|
|
75
|
+
|
|
76
|
+
db.onerror = (event) => {
|
|
77
|
+
logger.error('[SnapshotDB] Database error:', event);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
db.onclose = () => {
|
|
81
|
+
logger.info('[SnapshotDB] Connection closed');
|
|
82
|
+
this.db = null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
resolve(db);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
request.onupgradeneeded = (event) => {
|
|
89
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
90
|
+
|
|
91
|
+
// Create snapshots store if not exists
|
|
92
|
+
if (!db.objectStoreNames.contains(SNAPSHOTS_STORE)) {
|
|
93
|
+
const store = db.createObjectStore(SNAPSHOTS_STORE, {
|
|
94
|
+
keyPath: ['sessionId', 'documentId'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Create indexes for queries
|
|
98
|
+
store.createIndex('sessionId', 'sessionId', { unique: false });
|
|
99
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
100
|
+
|
|
101
|
+
logger.info('[SnapshotDB] Created document snapshots store');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
close(): void {
|
|
108
|
+
if (this.db) {
|
|
109
|
+
this.db.close();
|
|
110
|
+
this.db = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Singleton connection pool
|
|
116
|
+
const connectionPool = new SnapshotConnectionPool();
|
|
117
|
+
|
|
118
|
+
// Close on window unload
|
|
119
|
+
if (typeof window !== 'undefined') {
|
|
120
|
+
window.addEventListener('beforeunload', () => {
|
|
121
|
+
connectionPool.close();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* DocumentSnapshotService
|
|
127
|
+
* Manages document snapshots for comparison functionality
|
|
128
|
+
*/
|
|
129
|
+
export class DocumentSnapshotService {
|
|
130
|
+
/**
|
|
131
|
+
* Capture a snapshot of a document before processing
|
|
132
|
+
*
|
|
133
|
+
* @param buffer - The original document ArrayBuffer
|
|
134
|
+
* @param sessionId - Session identifier
|
|
135
|
+
* @param documentId - Document identifier
|
|
136
|
+
* @param textContent - Extracted paragraph text for diffing
|
|
137
|
+
* @param hyperlinks - Hyperlink state for comparison
|
|
138
|
+
*/
|
|
139
|
+
static async captureSnapshot(
|
|
140
|
+
buffer: ArrayBuffer,
|
|
141
|
+
sessionId: string,
|
|
142
|
+
documentId: string,
|
|
143
|
+
textContent: string[],
|
|
144
|
+
hyperlinks: HyperlinkSnapshot[] = []
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
const db = await connectionPool.getConnection();
|
|
148
|
+
|
|
149
|
+
const snapshot: SerializedDocumentSnapshot = {
|
|
150
|
+
documentId,
|
|
151
|
+
sessionId,
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
buffer,
|
|
154
|
+
textContent,
|
|
155
|
+
hyperlinkSnapshot: hyperlinks,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readwrite');
|
|
160
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
161
|
+
|
|
162
|
+
// Use put to overwrite existing snapshot for same session/document
|
|
163
|
+
const request = store.put(snapshot);
|
|
164
|
+
|
|
165
|
+
request.onsuccess = () => {
|
|
166
|
+
logger.info(
|
|
167
|
+
`[SnapshotDB] Captured snapshot for document ${documentId} ` +
|
|
168
|
+
`(${textContent.length} paragraphs, ${(buffer.byteLength / 1024).toFixed(1)}KB)`
|
|
169
|
+
);
|
|
170
|
+
resolve();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
request.onerror = () => {
|
|
174
|
+
logger.error('[SnapshotDB] Failed to capture snapshot:', request.error);
|
|
175
|
+
reject(new Error(`Failed to capture snapshot: ${request.error?.message}`));
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logger.error('[SnapshotDB] Error capturing snapshot:', error);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get a snapshot for a specific document
|
|
186
|
+
*
|
|
187
|
+
* @param sessionId - Session identifier
|
|
188
|
+
* @param documentId - Document identifier
|
|
189
|
+
* @returns The snapshot or null if not found
|
|
190
|
+
*/
|
|
191
|
+
static async getSnapshot(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
documentId: string
|
|
194
|
+
): Promise<DocumentSnapshot | null> {
|
|
195
|
+
try {
|
|
196
|
+
const db = await connectionPool.getConnection();
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readonly');
|
|
200
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
201
|
+
const request = store.get([sessionId, documentId]);
|
|
202
|
+
|
|
203
|
+
request.onsuccess = () => {
|
|
204
|
+
const result = request.result as SerializedDocumentSnapshot | undefined;
|
|
205
|
+
|
|
206
|
+
if (result) {
|
|
207
|
+
// Deserialize timestamp
|
|
208
|
+
const snapshot: DocumentSnapshot = {
|
|
209
|
+
...result,
|
|
210
|
+
timestamp: new Date(result.timestamp),
|
|
211
|
+
};
|
|
212
|
+
resolve(snapshot);
|
|
213
|
+
} else {
|
|
214
|
+
resolve(null);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
request.onerror = () => {
|
|
219
|
+
logger.error('[SnapshotDB] Failed to get snapshot:', request.error);
|
|
220
|
+
reject(new Error(`Failed to get snapshot: ${request.error?.message}`));
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error('[SnapshotDB] Error getting snapshot:', error);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Delete a snapshot for a specific document
|
|
231
|
+
*
|
|
232
|
+
* @param sessionId - Session identifier
|
|
233
|
+
* @param documentId - Document identifier
|
|
234
|
+
*/
|
|
235
|
+
static async deleteSnapshot(sessionId: string, documentId: string): Promise<void> {
|
|
236
|
+
try {
|
|
237
|
+
const db = await connectionPool.getConnection();
|
|
238
|
+
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readwrite');
|
|
241
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
242
|
+
const request = store.delete([sessionId, documentId]);
|
|
243
|
+
|
|
244
|
+
request.onsuccess = () => {
|
|
245
|
+
logger.info(`[SnapshotDB] Deleted snapshot for document ${documentId}`);
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
request.onerror = () => {
|
|
250
|
+
logger.error('[SnapshotDB] Failed to delete snapshot:', request.error);
|
|
251
|
+
reject(new Error(`Failed to delete snapshot: ${request.error?.message}`));
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.error('[SnapshotDB] Error deleting snapshot:', error);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete all snapshots for a session
|
|
262
|
+
*
|
|
263
|
+
* @param sessionId - Session identifier
|
|
264
|
+
* @returns Number of snapshots deleted
|
|
265
|
+
*/
|
|
266
|
+
static async cleanupSessionSnapshots(sessionId: string): Promise<number> {
|
|
267
|
+
try {
|
|
268
|
+
const db = await connectionPool.getConnection();
|
|
269
|
+
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readwrite');
|
|
272
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
273
|
+
const index = store.index('sessionId');
|
|
274
|
+
const request = index.getAllKeys(sessionId);
|
|
275
|
+
|
|
276
|
+
request.onsuccess = () => {
|
|
277
|
+
const keys = request.result;
|
|
278
|
+
let deletedCount = 0;
|
|
279
|
+
|
|
280
|
+
if (keys.length === 0) {
|
|
281
|
+
resolve(0);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
const deleteRequest = store.delete(key);
|
|
287
|
+
deleteRequest.onsuccess = () => {
|
|
288
|
+
deletedCount++;
|
|
289
|
+
if (deletedCount === keys.length) {
|
|
290
|
+
logger.info(`[SnapshotDB] Cleaned up ${deletedCount} snapshots for session ${sessionId}`);
|
|
291
|
+
resolve(deletedCount);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
request.onerror = () => {
|
|
298
|
+
reject(new Error(`Failed to cleanup session snapshots: ${request.error?.message}`));
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
logger.error('[SnapshotDB] Error cleaning up session snapshots:', error);
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Clean up old snapshots (older than MAX_SNAPSHOT_AGE_MS)
|
|
309
|
+
*
|
|
310
|
+
* @returns Number of snapshots deleted
|
|
311
|
+
*/
|
|
312
|
+
static async cleanupOldSnapshots(): Promise<number> {
|
|
313
|
+
try {
|
|
314
|
+
const db = await connectionPool.getConnection();
|
|
315
|
+
const cutoffDate = new Date(Date.now() - MAX_SNAPSHOT_AGE_MS).toISOString();
|
|
316
|
+
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readwrite');
|
|
319
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
320
|
+
const index = store.index('timestamp');
|
|
321
|
+
|
|
322
|
+
// Get all keys with timestamp before cutoff
|
|
323
|
+
const range = IDBKeyRange.upperBound(cutoffDate);
|
|
324
|
+
const request = index.getAllKeys(range);
|
|
325
|
+
|
|
326
|
+
request.onsuccess = () => {
|
|
327
|
+
const keys = request.result;
|
|
328
|
+
let deletedCount = 0;
|
|
329
|
+
|
|
330
|
+
if (keys.length === 0) {
|
|
331
|
+
resolve(0);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const key of keys) {
|
|
336
|
+
const deleteRequest = store.delete(key);
|
|
337
|
+
deleteRequest.onsuccess = () => {
|
|
338
|
+
deletedCount++;
|
|
339
|
+
if (deletedCount === keys.length) {
|
|
340
|
+
logger.info(`[SnapshotDB] Cleaned up ${deletedCount} old snapshots`);
|
|
341
|
+
resolve(deletedCount);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
request.onerror = () => {
|
|
348
|
+
reject(new Error(`Failed to cleanup old snapshots: ${request.error?.message}`));
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
} catch (error) {
|
|
352
|
+
logger.error('[SnapshotDB] Error cleaning up old snapshots:', error);
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get all snapshots for a session
|
|
359
|
+
*
|
|
360
|
+
* @param sessionId - Session identifier
|
|
361
|
+
* @returns Array of snapshots
|
|
362
|
+
*/
|
|
363
|
+
static async getSessionSnapshots(sessionId: string): Promise<DocumentSnapshot[]> {
|
|
364
|
+
try {
|
|
365
|
+
const db = await connectionPool.getConnection();
|
|
366
|
+
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readonly');
|
|
369
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
370
|
+
const index = store.index('sessionId');
|
|
371
|
+
const request = index.getAll(sessionId);
|
|
372
|
+
|
|
373
|
+
request.onsuccess = () => {
|
|
374
|
+
const results = request.result as SerializedDocumentSnapshot[];
|
|
375
|
+
const snapshots: DocumentSnapshot[] = results.map((r) => ({
|
|
376
|
+
...r,
|
|
377
|
+
timestamp: new Date(r.timestamp),
|
|
378
|
+
}));
|
|
379
|
+
resolve(snapshots);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
request.onerror = () => {
|
|
383
|
+
reject(new Error(`Failed to get session snapshots: ${request.error?.message}`));
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
logger.error('[SnapshotDB] Error getting session snapshots:', error);
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if a snapshot exists for a document
|
|
394
|
+
*
|
|
395
|
+
* @param sessionId - Session identifier
|
|
396
|
+
* @param documentId - Document identifier
|
|
397
|
+
* @returns True if snapshot exists
|
|
398
|
+
*/
|
|
399
|
+
static async hasSnapshot(sessionId: string, documentId: string): Promise<boolean> {
|
|
400
|
+
const snapshot = await this.getSnapshot(sessionId, documentId);
|
|
401
|
+
return snapshot !== null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Calculate total storage used by snapshots
|
|
406
|
+
*
|
|
407
|
+
* @returns Storage size in bytes
|
|
408
|
+
*/
|
|
409
|
+
static async calculateStorageSize(): Promise<number> {
|
|
410
|
+
try {
|
|
411
|
+
const db = await connectionPool.getConnection();
|
|
412
|
+
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readonly');
|
|
415
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
416
|
+
const request = store.getAll();
|
|
417
|
+
|
|
418
|
+
request.onsuccess = () => {
|
|
419
|
+
const snapshots = request.result as SerializedDocumentSnapshot[];
|
|
420
|
+
let totalSize = 0;
|
|
421
|
+
|
|
422
|
+
for (const snapshot of snapshots) {
|
|
423
|
+
// ArrayBuffer size
|
|
424
|
+
totalSize += snapshot.buffer.byteLength;
|
|
425
|
+
// Approximate JSON overhead
|
|
426
|
+
totalSize += JSON.stringify({
|
|
427
|
+
...snapshot,
|
|
428
|
+
buffer: null,
|
|
429
|
+
}).length * 2; // UTF-16
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
resolve(totalSize);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
request.onerror = () => {
|
|
436
|
+
reject(new Error(`Failed to calculate storage size: ${request.error?.message}`));
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
} catch (error) {
|
|
440
|
+
logger.error('[SnapshotDB] Error calculating storage size:', error);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get oldest snapshots sorted by timestamp
|
|
447
|
+
* Used for cleanup when storage exceeds limits
|
|
448
|
+
*/
|
|
449
|
+
static async getOldestSnapshots(limit: number): Promise<SerializedDocumentSnapshot[]> {
|
|
450
|
+
try {
|
|
451
|
+
const db = await connectionPool.getConnection();
|
|
452
|
+
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readonly');
|
|
455
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
456
|
+
const index = store.index('timestamp');
|
|
457
|
+
const request = index.getAll();
|
|
458
|
+
|
|
459
|
+
request.onsuccess = () => {
|
|
460
|
+
const snapshots = request.result as SerializedDocumentSnapshot[];
|
|
461
|
+
// Sort by timestamp (oldest first) and take limit
|
|
462
|
+
const sorted = snapshots
|
|
463
|
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
464
|
+
.slice(0, limit);
|
|
465
|
+
resolve(sorted);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
request.onerror = () => {
|
|
469
|
+
reject(new Error(`Failed to get oldest snapshots: ${request.error?.message}`));
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
logger.error('[SnapshotDB] Error getting oldest snapshots:', error);
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Delete snapshots by session and document ID pairs
|
|
480
|
+
*/
|
|
481
|
+
static async deleteSnapshots(
|
|
482
|
+
snapshots: Array<{ sessionId: string; documentId: string }>
|
|
483
|
+
): Promise<number> {
|
|
484
|
+
if (snapshots.length === 0) return 0;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const db = await connectionPool.getConnection();
|
|
488
|
+
let deletedCount = 0;
|
|
489
|
+
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const transaction = db.transaction([SNAPSHOTS_STORE], 'readwrite');
|
|
492
|
+
const store = transaction.objectStore(SNAPSHOTS_STORE);
|
|
493
|
+
|
|
494
|
+
for (const { sessionId, documentId } of snapshots) {
|
|
495
|
+
const request = store.delete([sessionId, documentId]);
|
|
496
|
+
request.onsuccess = () => {
|
|
497
|
+
deletedCount++;
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
transaction.oncomplete = () => {
|
|
502
|
+
logger.info(`[SnapshotDB] Deleted ${deletedCount} snapshots`);
|
|
503
|
+
resolve(deletedCount);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
transaction.onerror = () => {
|
|
507
|
+
reject(new Error(`Failed to delete snapshots: ${transaction.error?.message}`));
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
} catch (error) {
|
|
511
|
+
logger.error('[SnapshotDB] Error deleting snapshots:', error);
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Ensure storage is within limits, cleaning up if necessary
|
|
518
|
+
* Implements oldest-first deletion strategy
|
|
519
|
+
*/
|
|
520
|
+
static async ensureStorageLimit(): Promise<void> {
|
|
521
|
+
const currentSize = await this.calculateStorageSize();
|
|
522
|
+
|
|
523
|
+
if (currentSize > MAX_STORAGE_BYTES) {
|
|
524
|
+
logger.warn(
|
|
525
|
+
`[SnapshotDB] Storage ${(currentSize / 1024 / 1024).toFixed(2)}MB ` +
|
|
526
|
+
`exceeds limit ${(MAX_STORAGE_BYTES / 1024 / 1024).toFixed(0)}MB`
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// First, clean up old snapshots (older than MAX_SNAPSHOT_AGE_MS)
|
|
530
|
+
await this.cleanupOldSnapshots();
|
|
531
|
+
|
|
532
|
+
// Check again and delete more if needed using oldest-first strategy
|
|
533
|
+
let newSize = await this.calculateStorageSize();
|
|
534
|
+
let iterationCount = 0;
|
|
535
|
+
const maxIterations = 10; // Safety limit
|
|
536
|
+
|
|
537
|
+
while (newSize > MAX_STORAGE_BYTES && iterationCount < maxIterations) {
|
|
538
|
+
logger.warn('[SnapshotDB] Still over limit, deleting oldest snapshots');
|
|
539
|
+
|
|
540
|
+
// Get oldest snapshots (batch of 5)
|
|
541
|
+
const oldestSnapshots = await this.getOldestSnapshots(5);
|
|
542
|
+
|
|
543
|
+
if (oldestSnapshots.length === 0) {
|
|
544
|
+
logger.warn('[SnapshotDB] No more snapshots to delete');
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Delete oldest snapshots
|
|
549
|
+
const toDelete = oldestSnapshots.map((s) => ({
|
|
550
|
+
sessionId: s.sessionId,
|
|
551
|
+
documentId: s.documentId,
|
|
552
|
+
}));
|
|
553
|
+
|
|
554
|
+
const deletedCount = await this.deleteSnapshots(toDelete);
|
|
555
|
+
logger.info(`[SnapshotDB] Deleted ${deletedCount} oldest snapshots to free space`);
|
|
556
|
+
|
|
557
|
+
// Recalculate size
|
|
558
|
+
newSize = await this.calculateStorageSize();
|
|
559
|
+
logger.debug(
|
|
560
|
+
`[SnapshotDB] Size after cleanup: ${(newSize / 1024 / 1024).toFixed(2)}MB`
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
iterationCount++;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (iterationCount >= maxIterations) {
|
|
567
|
+
logger.warn('[SnapshotDB] Max cleanup iterations reached');
|
|
568
|
+
} else if (newSize <= MAX_STORAGE_BYTES) {
|
|
569
|
+
logger.info('[SnapshotDB] Storage now within limits');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export default DocumentSnapshotService;
|