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,290 @@
1
+ import { createContext, useContext, useState, useEffect, ReactNode, useCallback, useMemo } from 'react';
2
+ import { UserSettings, defaultUserSettings } from '@/types/settings';
3
+ import { logger } from '@/utils/logger';
4
+ import { safeJsonParse, safeJsonStringify } from '@/utils/safeJsonParse';
5
+
6
+ interface UserSettingsContextType {
7
+ settings: UserSettings;
8
+ isLoading: boolean;
9
+ updateSettings: (updates: Partial<UserSettings>) => void;
10
+ updateProfile: (updates: Partial<UserSettings['profile']>) => void;
11
+ updateNotifications: (updates: Partial<UserSettings['notifications']>) => void;
12
+ updateApiConnections: (updates: Partial<UserSettings['apiConnections']>) => void;
13
+ updateUpdateSettings: (updates: Partial<UserSettings['updateSettings']>) => void;
14
+ updateLocalDictionary: (updates: Partial<UserSettings['localDictionary']>) => void;
15
+ updateBackupSettings: (updates: Partial<UserSettings['backupSettings']>) => void;
16
+ updateDisplaySettings: (updates: Partial<UserSettings['displaySettings']>) => void;
17
+ saveSettings: () => Promise<boolean>;
18
+ loadSettings: () => void;
19
+ resetSettings: () => void;
20
+ }
21
+
22
+ const UserSettingsContext = createContext<UserSettingsContextType | undefined>(undefined);
23
+
24
+ const STORAGE_KEY = 'userSettings';
25
+
26
+ export function UserSettingsProvider({ children }: { children: ReactNode }) {
27
+ const log = logger.namespace('UserSettings');
28
+ const [settings, setSettings] = useState<UserSettings>(defaultUserSettings);
29
+ const [isLoading, setIsLoading] = useState(true);
30
+
31
+ const loadSettings = useCallback(() => {
32
+ setIsLoading(true);
33
+ try {
34
+ const storedSettings = localStorage.getItem(STORAGE_KEY);
35
+ const parsed = safeJsonParse<Partial<UserSettings>>(
36
+ storedSettings,
37
+ {},
38
+ 'UserSettings.loadSettings'
39
+ );
40
+ setSettings({ ...defaultUserSettings, ...parsed });
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ }, []);
45
+
46
+ const saveSettings = useCallback(async (): Promise<boolean> => {
47
+ const jsonString = safeJsonStringify(settings, undefined, 'UserSettings.saveSettings');
48
+ if (jsonString) {
49
+ try {
50
+ localStorage.setItem(STORAGE_KEY, jsonString);
51
+ return true;
52
+ } catch (error) {
53
+ log.error('Failed to save user settings to localStorage:', error);
54
+ return false;
55
+ }
56
+ }
57
+ return false;
58
+ }, [settings, log]);
59
+
60
+ const updateSettings = useCallback((updates: Partial<UserSettings>) => {
61
+ setSettings((prev) => {
62
+ const newSettings = { ...prev, ...updates };
63
+ // Auto-save to localStorage
64
+ const jsonString = safeJsonStringify(newSettings, undefined, 'UserSettings.updateSettings');
65
+ if (jsonString) {
66
+ try {
67
+ localStorage.setItem(STORAGE_KEY, jsonString);
68
+ } catch (error) {
69
+ // Silent fail - logged elsewhere
70
+ }
71
+ }
72
+ return newSettings;
73
+ });
74
+ }, []);
75
+
76
+ const updateProfile = useCallback((updates: Partial<UserSettings['profile']>) => {
77
+ setSettings((prev) => {
78
+ const newSettings = {
79
+ ...prev,
80
+ profile: { ...prev.profile, ...updates },
81
+ };
82
+ // Auto-save profile settings to localStorage
83
+ const jsonString = safeJsonStringify(
84
+ newSettings,
85
+ undefined,
86
+ 'UserSettings.updateProfile'
87
+ );
88
+ if (jsonString) {
89
+ try {
90
+ localStorage.setItem(STORAGE_KEY, jsonString);
91
+ } catch (error) {
92
+ // Silent fail - logged elsewhere
93
+ }
94
+ }
95
+ return newSettings;
96
+ });
97
+ }, []);
98
+
99
+ const updateNotifications = useCallback((updates: Partial<UserSettings['notifications']>) => {
100
+ setSettings((prev) => {
101
+ const newSettings = {
102
+ ...prev,
103
+ notifications: { ...prev.notifications, ...updates },
104
+ };
105
+ // Auto-save notification settings to localStorage
106
+ const jsonString = safeJsonStringify(
107
+ newSettings,
108
+ undefined,
109
+ 'UserSettings.updateNotifications'
110
+ );
111
+ if (jsonString) {
112
+ try {
113
+ localStorage.setItem(STORAGE_KEY, jsonString);
114
+ } catch (error) {
115
+ // Silent fail - logged elsewhere
116
+ }
117
+ }
118
+ return newSettings;
119
+ });
120
+ }, []);
121
+
122
+ const updateApiConnections = useCallback((updates: Partial<UserSettings['apiConnections']>) => {
123
+ setSettings((prev) => {
124
+ const newSettings = {
125
+ ...prev,
126
+ apiConnections: { ...prev.apiConnections, ...updates },
127
+ };
128
+ // Auto-save API settings to localStorage
129
+ const jsonString = safeJsonStringify(
130
+ newSettings,
131
+ undefined,
132
+ 'UserSettings.updateApiConnections'
133
+ );
134
+ if (jsonString) {
135
+ try {
136
+ localStorage.setItem(STORAGE_KEY, jsonString);
137
+ } catch (error) {
138
+ // Silent fail - logged elsewhere
139
+ }
140
+ }
141
+ return newSettings;
142
+ });
143
+ }, []);
144
+
145
+ const updateUpdateSettings = useCallback((updates: Partial<UserSettings['updateSettings']>) => {
146
+ setSettings((prev) => {
147
+ const newSettings = {
148
+ ...prev,
149
+ updateSettings: { ...prev.updateSettings, ...updates },
150
+ };
151
+ // Auto-save update settings to localStorage
152
+ const jsonString = safeJsonStringify(
153
+ newSettings,
154
+ undefined,
155
+ 'UserSettings.updateUpdateSettings'
156
+ );
157
+ if (jsonString) {
158
+ try {
159
+ localStorage.setItem(STORAGE_KEY, jsonString);
160
+ } catch (error) {
161
+ // Silent fail - logged elsewhere
162
+ }
163
+ }
164
+ return newSettings;
165
+ });
166
+ }, []);
167
+
168
+ const updateLocalDictionary = useCallback((updates: Partial<UserSettings['localDictionary']>) => {
169
+ setSettings((prev) => {
170
+ const newSettings = {
171
+ ...prev,
172
+ localDictionary: { ...prev.localDictionary, ...updates },
173
+ };
174
+ // Auto-save local dictionary settings to localStorage
175
+ const jsonString = safeJsonStringify(
176
+ newSettings,
177
+ undefined,
178
+ 'UserSettings.updateLocalDictionary'
179
+ );
180
+ if (jsonString) {
181
+ try {
182
+ localStorage.setItem(STORAGE_KEY, jsonString);
183
+ } catch (error) {
184
+ // Silent fail - logged elsewhere
185
+ }
186
+ }
187
+ return newSettings;
188
+ });
189
+ }, []);
190
+
191
+ const updateBackupSettings = useCallback((updates: Partial<UserSettings['backupSettings']>) => {
192
+ setSettings((prev) => {
193
+ const newSettings = {
194
+ ...prev,
195
+ backupSettings: { ...prev.backupSettings, ...updates },
196
+ };
197
+ // Auto-save backup settings to localStorage
198
+ const jsonString = safeJsonStringify(
199
+ newSettings,
200
+ undefined,
201
+ 'UserSettings.updateBackupSettings'
202
+ );
203
+ if (jsonString) {
204
+ try {
205
+ localStorage.setItem(STORAGE_KEY, jsonString);
206
+ } catch (error) {
207
+ // Silent fail - logged elsewhere
208
+ }
209
+ }
210
+ return newSettings;
211
+ });
212
+ }, []);
213
+
214
+ const updateDisplaySettings = useCallback((updates: Partial<UserSettings['displaySettings']>) => {
215
+ setSettings((prev) => {
216
+ const newSettings = {
217
+ ...prev,
218
+ displaySettings: { ...prev.displaySettings, ...updates },
219
+ };
220
+ // Auto-save display settings to localStorage
221
+ const jsonString = safeJsonStringify(
222
+ newSettings,
223
+ undefined,
224
+ 'UserSettings.updateDisplaySettings'
225
+ );
226
+ if (jsonString) {
227
+ try {
228
+ localStorage.setItem(STORAGE_KEY, jsonString);
229
+ } catch (error) {
230
+ // Silent fail - logged elsewhere
231
+ }
232
+ }
233
+ return newSettings;
234
+ });
235
+ }, []);
236
+
237
+ const resetSettings = useCallback(() => {
238
+ setSettings(defaultUserSettings);
239
+ localStorage.removeItem(STORAGE_KEY);
240
+ }, []);
241
+
242
+ useEffect(() => {
243
+ loadSettings();
244
+ }, [loadSettings]);
245
+
246
+ // PERFORMANCE FIX: Memoize context value to prevent unnecessary re-renders in consumers
247
+ // Without this, every render creates a new object reference, causing all consumers to re-render
248
+ const value = useMemo<UserSettingsContextType>(
249
+ () => ({
250
+ settings,
251
+ isLoading,
252
+ updateSettings,
253
+ updateProfile,
254
+ updateNotifications,
255
+ updateApiConnections,
256
+ updateUpdateSettings,
257
+ updateLocalDictionary,
258
+ updateBackupSettings,
259
+ updateDisplaySettings,
260
+ saveSettings,
261
+ loadSettings,
262
+ resetSettings,
263
+ }),
264
+ [
265
+ settings,
266
+ isLoading,
267
+ updateSettings,
268
+ updateProfile,
269
+ updateNotifications,
270
+ updateApiConnections,
271
+ updateUpdateSettings,
272
+ updateLocalDictionary,
273
+ updateBackupSettings,
274
+ updateDisplaySettings,
275
+ saveSettings,
276
+ loadSettings,
277
+ resetSettings,
278
+ ]
279
+ );
280
+
281
+ return <UserSettingsContext.Provider value={value}>{children}</UserSettingsContext.Provider>;
282
+ }
283
+
284
+ export function useUserSettings() {
285
+ const context = useContext(UserSettingsContext);
286
+ if (!context) {
287
+ throw new Error('useUserSettings must be used within a UserSettingsProvider');
288
+ }
289
+ return context;
290
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Test Suite for GlobalStatsContext
3
+ *
4
+ * Tests for Issue #3: Memory Leak Prevention
5
+ * - Ensures connection pool is used (no duplicate connections)
6
+ * - Validates proper cleanup on unmount
7
+ * - Tests singleton pattern enforcement
8
+ * - Verifies connection reuse across operations
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, renderHook, waitFor, act, cleanup } from '@testing-library/react';
13
+ import { GlobalStatsProvider, useGlobalStats } from '../GlobalStatsContext';
14
+ import * as indexedDB from '@/utils/indexedDB';
15
+ import { createDefaultGlobalStats } from '@/types/globalStats';
16
+
17
+ // Mock IndexedDB utilities
18
+ jest.mock('@/utils/indexedDB', () => ({
19
+ loadGlobalStats: jest.fn(),
20
+ saveGlobalStats: jest.fn(),
21
+ resetGlobalStats: jest.fn(),
22
+ getGlobalStatsConnectionPool: jest.fn(),
23
+ }));
24
+
25
+ // Mock logger
26
+ jest.mock('@/utils/logger', () => ({
27
+ logger: {
28
+ namespace: () => ({
29
+ info: jest.fn(),
30
+ warn: jest.fn(),
31
+ error: jest.fn(),
32
+ debug: jest.fn(),
33
+ }),
34
+ },
35
+ }));
36
+
37
+ describe('GlobalStatsContext - Issue #3: Memory Leak Prevention', () => {
38
+ const mockConnectionPool = {
39
+ getConnection: jest.fn(),
40
+ close: jest.fn(),
41
+ getStats: jest.fn().mockReturnValue({
42
+ connected: true,
43
+ reconnectAttempts: 0,
44
+ lastError: null,
45
+ }),
46
+ };
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ // Flush lingering timers from previous tests (debounced saves use 1000ms setTimeout)
51
+ jest.clearAllTimers();
52
+ const freshStats = createDefaultGlobalStats();
53
+ (indexedDB.loadGlobalStats as jest.Mock).mockResolvedValue(freshStats);
54
+ (indexedDB.saveGlobalStats as jest.Mock).mockResolvedValue(undefined);
55
+ (indexedDB.resetGlobalStats as jest.Mock).mockResolvedValue(undefined);
56
+ (indexedDB.getGlobalStatsConnectionPool as jest.Mock).mockReturnValue(mockConnectionPool);
57
+ });
58
+
59
+ afterEach(async () => {
60
+ // Ensure all React trees are unmounted before clearing mocks
61
+ cleanup();
62
+ // Allow pending microtasks (async effects) to settle
63
+ await act(async () => {
64
+ await new Promise((r) => setTimeout(r, 0));
65
+ });
66
+ jest.clearAllTimers();
67
+ jest.restoreAllMocks();
68
+ });
69
+
70
+ describe('Connection Pool Usage', () => {
71
+ it('should use connection pool singleton for all operations', async () => {
72
+ const { result } = renderHook(() => useGlobalStats(), {
73
+ wrapper: GlobalStatsProvider,
74
+ });
75
+
76
+ await waitFor(() => {
77
+ expect(result.current).toBeTruthy();
78
+ expect(result.current?.isLoading).toBe(false);
79
+ });
80
+
81
+ // Verify loadGlobalStats was called (uses connection pool internally)
82
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalledTimes(1);
83
+
84
+ // Update stats multiple times
85
+ await act(async () => {
86
+ await result.current?.updateStats({ documentsProcessed: 1 });
87
+ });
88
+
89
+ await act(async () => {
90
+ await result.current?.updateStats({ hyperlinksChecked: 5 });
91
+ });
92
+
93
+ await act(async () => {
94
+ await result.current?.updateStats({ timeSaved: 30 });
95
+ });
96
+
97
+ // All operations should use the same connection pool
98
+ // saveGlobalStats is called: initial load (if new) + 3 updates
99
+ expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
100
+ // Connection pool reused - no new connections created
101
+ });
102
+
103
+ it('should not create duplicate connections on multiple renders', async () => {
104
+ const { rerender } = render(
105
+ <GlobalStatsProvider>
106
+ <div>Test</div>
107
+ </GlobalStatsProvider>
108
+ );
109
+
110
+ await waitFor(() => {
111
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalledTimes(1);
112
+ });
113
+
114
+ // Force re-render
115
+ rerender(
116
+ <GlobalStatsProvider>
117
+ <div>Test Updated</div>
118
+ </GlobalStatsProvider>
119
+ );
120
+
121
+ // Should not create new connection - still using same one
122
+ await waitFor(() => {
123
+ // loadGlobalStats might be called again, but should reuse connection
124
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
125
+ });
126
+ });
127
+
128
+ it('should reuse connection across state updates', async () => {
129
+ const connectionCallsBefore = mockConnectionPool.getConnection.mock.calls.length;
130
+
131
+ const { result } = renderHook(() => useGlobalStats(), {
132
+ wrapper: GlobalStatsProvider,
133
+ });
134
+
135
+ await waitFor(() => {
136
+ expect(result.current).toBeTruthy();
137
+ expect(result.current?.isLoading).toBe(false);
138
+ });
139
+
140
+ // Perform 10 consecutive updates
141
+ for (let i = 0; i < 10; i++) {
142
+ await act(async () => {
143
+ await result.current?.updateStats({ documentsProcessed: 1 });
144
+ });
145
+ }
146
+
147
+ // Connection pool should be reused, not recreated
148
+ // We don't directly call getConnection in our context,
149
+ // but indexedDB functions use it internally
150
+ expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
151
+ });
152
+ });
153
+
154
+ describe('Memory Leak Prevention', () => {
155
+ it('should properly cleanup on unmount', async () => {
156
+ const { unmount } = renderHook(() => useGlobalStats(), {
157
+ wrapper: GlobalStatsProvider,
158
+ });
159
+
160
+ await waitFor(() => {
161
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
162
+ });
163
+
164
+ // Unmount component
165
+ unmount();
166
+
167
+ // Cleanup flag should prevent state updates after unmount
168
+ // (verified by useEffect cleanup setting isMounted = false)
169
+ });
170
+
171
+ it('should not create memory leak with rapid mount/unmount', async () => {
172
+ const mockLoadGlobalStats = indexedDB.loadGlobalStats as jest.Mock;
173
+ const connectionsBefore = mockLoadGlobalStats.mock.calls.length;
174
+
175
+ // Mount and unmount 20 times rapidly
176
+ for (let i = 0; i < 20; i++) {
177
+ const { unmount } = renderHook(() => useGlobalStats(), {
178
+ wrapper: GlobalStatsProvider,
179
+ });
180
+
181
+ await waitFor(() => {
182
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
183
+ });
184
+
185
+ unmount();
186
+ }
187
+
188
+ // Should not have created 20 separate connections
189
+ // Connection pool should be reused
190
+ const connectionsAfter = mockLoadGlobalStats.mock.calls.length;
191
+
192
+ // Each mount calls loadGlobalStats once, but uses same connection pool
193
+ expect(connectionsAfter).toBeGreaterThanOrEqual(connectionsBefore);
194
+ });
195
+
196
+ it('should handle concurrent updates without connection leaks', async () => {
197
+ const { result, unmount } = renderHook(() => useGlobalStats(), {
198
+ wrapper: GlobalStatsProvider,
199
+ });
200
+
201
+ await waitFor(() => {
202
+ expect(result.current).toBeTruthy();
203
+ expect(result.current?.isLoading).toBe(false);
204
+ });
205
+
206
+ // Fire rapid sequential updates (tests debounce behavior)
207
+ for (let i = 0; i < 10; i++) {
208
+ await act(async () => {
209
+ await result.current?.updateStats({ documentsProcessed: 1 });
210
+ });
211
+ }
212
+
213
+ // All updates should complete without errors
214
+ // Connection pool handles concurrency correctly
215
+ expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
216
+
217
+ // Clean up to prevent leaking into next test
218
+ unmount();
219
+ });
220
+ });
221
+
222
+ describe('Connection Pool Singleton', () => {
223
+ it('should enforce singleton pattern across multiple providers', async () => {
224
+ // Even if we create multiple provider instances,
225
+ // they should all use the same connection pool
226
+
227
+ const { result: result1 } = renderHook(() => useGlobalStats(), {
228
+ wrapper: ({ children }) => (
229
+ <GlobalStatsProvider key="provider1">{children}</GlobalStatsProvider>
230
+ ),
231
+ });
232
+
233
+ const { result: result2 } = renderHook(() => useGlobalStats(), {
234
+ wrapper: ({ children }) => (
235
+ <GlobalStatsProvider key="provider2">{children}</GlobalStatsProvider>
236
+ ),
237
+ });
238
+
239
+ await waitFor(() => {
240
+ expect(result1.current).toBeTruthy();
241
+ expect(result1.current?.isLoading).toBe(false);
242
+ expect(result2.current).toBeTruthy();
243
+ expect(result2.current?.isLoading).toBe(false);
244
+ });
245
+
246
+ // Both should have loaded stats
247
+ expect(indexedDB.loadGlobalStats).toHaveBeenCalled();
248
+
249
+ // Update from both providers
250
+ await act(async () => {
251
+ await result1.current?.updateStats({ documentsProcessed: 1 });
252
+ });
253
+
254
+ await act(async () => {
255
+ await result2.current?.updateStats({ hyperlinksChecked: 5 });
256
+ });
257
+
258
+ // Both use same connection pool
259
+ expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
260
+ });
261
+
262
+ it('should retrieve connection pool instance correctly', () => {
263
+ const pool = indexedDB.getGlobalStatsConnectionPool();
264
+
265
+ expect(pool).toBeDefined();
266
+ expect(pool).toBe(mockConnectionPool);
267
+ expect(typeof pool.getConnection).toBe('function');
268
+ expect(typeof pool.close).toBe('function');
269
+ expect(typeof pool.getStats).toBe('function');
270
+ });
271
+ });
272
+
273
+ describe('Functional Tests', () => {
274
+ it('should initialize with default stats', async () => {
275
+ const { result } = renderHook(() => useGlobalStats(), {
276
+ wrapper: GlobalStatsProvider,
277
+ });
278
+
279
+ await waitFor(() => {
280
+ expect(result.current).toBeTruthy();
281
+ expect(result.current?.isLoading).toBe(false);
282
+ });
283
+
284
+ expect(result.current?.stats).toBeDefined();
285
+ expect(result.current?.stats.allTime).toBeDefined();
286
+ expect(result.current?.stats.today).toBeDefined();
287
+ });
288
+
289
+ it('should update stats correctly', async () => {
290
+ const { result } = renderHook(() => useGlobalStats(), {
291
+ wrapper: GlobalStatsProvider,
292
+ });
293
+
294
+ await waitFor(() => {
295
+ expect(result.current).toBeTruthy();
296
+ expect(result.current?.isLoading).toBe(false);
297
+ });
298
+
299
+ const initialDocsProcessed = result.current?.stats.allTime.documentsProcessed;
300
+
301
+ await act(async () => {
302
+ await result.current?.updateStats({ documentsProcessed: 5 });
303
+ });
304
+
305
+ expect(result.current?.stats.allTime.documentsProcessed).toBe(initialDocsProcessed + 5);
306
+ });
307
+
308
+ it('should persist stats to IndexedDB on update', async () => {
309
+ const { result } = renderHook(() => useGlobalStats(), {
310
+ wrapper: GlobalStatsProvider,
311
+ });
312
+
313
+ await waitFor(() => {
314
+ expect(result.current).toBeTruthy();
315
+ expect(result.current?.isLoading).toBe(false);
316
+ });
317
+
318
+ await act(async () => {
319
+ await result.current?.updateStats({ documentsProcessed: 3 });
320
+ });
321
+
322
+ // saveGlobalStats should be called with updated stats
323
+ expect(indexedDB.saveGlobalStats).toHaveBeenCalled();
324
+ const savedStats = (indexedDB.saveGlobalStats as jest.Mock).mock.calls[0][0];
325
+ expect(savedStats.allTime.documentsProcessed).toBeGreaterThan(0);
326
+ });
327
+
328
+ it('should reset stats correctly', async () => {
329
+ const { result } = renderHook(() => useGlobalStats(), {
330
+ wrapper: GlobalStatsProvider,
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(result.current).toBeTruthy();
335
+ expect(result.current?.isLoading).toBe(false);
336
+ });
337
+
338
+ await act(async () => {
339
+ await result.current?.updateStats({ documentsProcessed: 10 });
340
+ });
341
+
342
+ await act(async () => {
343
+ await result.current?.resetAllStats();
344
+ });
345
+
346
+ expect(indexedDB.resetGlobalStats).toHaveBeenCalled();
347
+ });
348
+ });
349
+
350
+ describe('Error Handling', () => {
351
+ it('should handle load errors gracefully', async () => {
352
+ (indexedDB.loadGlobalStats as jest.Mock).mockRejectedValue(
353
+ new Error('Database connection failed')
354
+ );
355
+
356
+ const { result } = renderHook(() => useGlobalStats(), {
357
+ wrapper: GlobalStatsProvider,
358
+ });
359
+
360
+ await waitFor(() => {
361
+ expect(result.current).toBeTruthy();
362
+ expect(result.current?.isLoading).toBe(false);
363
+ });
364
+
365
+ // Should still have default stats despite error
366
+ expect(result.current?.stats).toBeDefined();
367
+ });
368
+
369
+ it('should handle save errors without crashing', async () => {
370
+ (indexedDB.saveGlobalStats as jest.Mock).mockRejectedValue(new Error('Quota exceeded'));
371
+
372
+ const { result } = renderHook(() => useGlobalStats(), {
373
+ wrapper: GlobalStatsProvider,
374
+ });
375
+
376
+ await waitFor(() => {
377
+ expect(result.current).toBeTruthy();
378
+ expect(result.current?.isLoading).toBe(false);
379
+ });
380
+
381
+ // Update should not crash even if save fails
382
+ await act(async () => {
383
+ await result.current?.updateStats({ documentsProcessed: 1 });
384
+ });
385
+
386
+ // State should still be updated locally
387
+ expect(result.current?.stats.allTime.documentsProcessed).toBeGreaterThan(0);
388
+ });
389
+ });
390
+ });