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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. 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;