@jmruthers/pace-core 0.5.185 → 0.5.187

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 (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -11,7 +11,7 @@ import {
11
11
  FileCategory
12
12
  } from '../types/file-reference';
13
13
  import { createFileReferenceService, uploadFileWithReference } from '../utils/file-reference';
14
- import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
14
+ import { getPublicUrl, getSignedUrl, generateFileUrlsBatch } from '../utils/storage/helpers';
15
15
  import { createLogger } from '../utils/core/logger';
16
16
 
17
17
  const log = createLogger('useFileReference');
@@ -401,33 +401,12 @@ export function useFilesByCategory(
401
401
  const files = await getFilesByCategory(table_name, record_id, category, organisation_id);
402
402
  setFileReferences(files);
403
403
 
404
- // Load URLs for all files
405
- const urlMap = new Map<string, string>();
406
-
407
- for (const fileRef of files) {
408
- try {
409
- let url: string | null = null;
410
-
411
- if (fileRef.is_public) {
412
- // Public files: generate public URL
413
- url = getPublicUrl(supabase, fileRef.file_path, true);
414
- } else {
415
- // Private files: generate signed URL
416
- const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
404
+ // Load URLs for all files in batch
405
+ const urlMap = await generateFileUrlsBatch(supabase, files, {
417
406
  appName: 'file-reference',
418
407
  orgId: organisation_id,
419
408
  expiresIn: 3600
420
409
  });
421
- url = signedUrlResult?.url || null;
422
- }
423
-
424
- if (url) {
425
- urlMap.set(fileRef.id, url);
426
- }
427
- } catch (err) {
428
- log.error(`Failed to load URL for file ${fileRef.id}:`, err);
429
- }
430
- }
431
410
 
432
411
  setFileUrls(urlMap);
433
412
  return files;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @file File URL Cache Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks
5
+ *
6
+ * Centralized caching hook for file URLs to prevent duplicate requests
7
+ * and improve performance across components.
8
+ *
9
+ * Features:
10
+ * - TTL-based caching matching signed URL expiration (3600s)
11
+ * - Automatic cache cleanup
12
+ * - Supports both public and signed URLs
13
+ * - Thread-safe cache operations
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import { useFileUrlCache } from '@jmruthers/pace-core';
18
+ *
19
+ * function MyComponent() {
20
+ * const { getUrl, setUrl, clearCache } = useFileUrlCache();
21
+ *
22
+ * const url = await getUrl(fileReference, supabase, organisationId);
23
+ * return <img src={url} alt="File" />;
24
+ * }
25
+ * ```
26
+ */
27
+
28
+ import { useRef, useCallback } from 'react';
29
+ import type { SupabaseClient } from '@supabase/supabase-js';
30
+ import { FileReference } from '../types/file-reference';
31
+ import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
32
+
33
+ interface CachedUrl {
34
+ url: string;
35
+ expiresAt: number; // Timestamp in milliseconds
36
+ }
37
+
38
+ // Global cache shared across all hook instances
39
+ const globalUrlCache = new Map<string, CachedUrl>();
40
+
41
+ // Cache size limit to prevent memory leaks
42
+ const MAX_CACHE_SIZE = 500;
43
+
44
+ // Default TTL matches signed URL expiration (3600 seconds = 1 hour)
45
+ const DEFAULT_TTL_MS = 3600 * 1000;
46
+
47
+ /**
48
+ * Generate cache key from file reference
49
+ */
50
+ function getCacheKey(fileReference: FileReference): string {
51
+ return `file-url:${fileReference.id}:${fileReference.is_public ? 'public' : 'private'}`;
52
+ }
53
+
54
+ /**
55
+ * Clean up expired entries and enforce size limit
56
+ */
57
+ function cleanupCache(): void {
58
+ const now = Date.now();
59
+
60
+ // Remove expired entries
61
+ for (const [key, value] of globalUrlCache.entries()) {
62
+ if (value.expiresAt < now) {
63
+ globalUrlCache.delete(key);
64
+ }
65
+ }
66
+
67
+ // Enforce size limit by removing oldest entries
68
+ if (globalUrlCache.size > MAX_CACHE_SIZE) {
69
+ const entries = Array.from(globalUrlCache.entries());
70
+ // Sort by expiration time (oldest first)
71
+ entries.sort((a, b) => a[1].expiresAt - b[1].expiresAt);
72
+
73
+ // Remove oldest 20% of entries
74
+ const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
75
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
76
+ globalUrlCache.delete(entries[i][0]);
77
+ }
78
+ }
79
+ }
80
+
81
+ export interface UseFileUrlCacheReturn {
82
+ /**
83
+ * Get URL for a file reference, using cache if available
84
+ * @param fileReference - File reference to get URL for
85
+ * @param supabase - Supabase client instance
86
+ * @param organisationId - Organisation ID for signed URLs
87
+ * @param ttl - Time to live in milliseconds (default: 3600000 = 1 hour)
88
+ * @returns Promise resolving to URL string or null
89
+ */
90
+ getUrl: (
91
+ fileReference: FileReference,
92
+ supabase: SupabaseClient,
93
+ organisationId: string,
94
+ ttl?: number
95
+ ) => Promise<string | null>;
96
+
97
+ /**
98
+ * Set URL in cache
99
+ * @param fileReference - File reference
100
+ * @param url - URL to cache
101
+ * @param ttl - Time to live in milliseconds (default: 3600000 = 1 hour)
102
+ */
103
+ setUrl: (fileReference: FileReference, url: string, ttl?: number) => void;
104
+
105
+ /**
106
+ * Get URL from cache without generating if missing
107
+ * @param fileReference - File reference
108
+ * @returns Cached URL or null if not in cache or expired
109
+ */
110
+ getCachedUrl: (fileReference: FileReference) => string | null;
111
+
112
+ /**
113
+ * Clear cache for a specific file reference
114
+ * @param fileReference - File reference to clear
115
+ */
116
+ clearFile: (fileReference: FileReference) => void;
117
+
118
+ /**
119
+ * Clear all cached URLs
120
+ */
121
+ clearCache: () => void;
122
+
123
+ /**
124
+ * Get cache statistics
125
+ */
126
+ getCacheStats: () => { size: number; maxSize: number };
127
+ }
128
+
129
+ /**
130
+ * Hook for centralized file URL caching
131
+ *
132
+ * This hook provides a shared cache for file URLs across all components,
133
+ * preventing duplicate requests for the same file.
134
+ *
135
+ * @returns Cache operations and utilities
136
+ */
137
+ export function useFileUrlCache(): UseFileUrlCacheReturn {
138
+ // Use ref to ensure stable reference across renders
139
+ const cleanupIntervalRef = useRef<number | null>(null);
140
+
141
+ // Set up periodic cleanup (every 5 minutes)
142
+ if (cleanupIntervalRef.current === null && typeof window !== 'undefined') {
143
+ cleanupIntervalRef.current = window.setInterval(() => {
144
+ cleanupCache();
145
+ }, 5 * 60 * 1000); // 5 minutes
146
+ }
147
+
148
+ const getUrl = useCallback(async (
149
+ fileReference: FileReference,
150
+ supabase: SupabaseClient,
151
+ organisationId: string,
152
+ ttl: number = DEFAULT_TTL_MS
153
+ ): Promise<string | null> => {
154
+ const cacheKey = getCacheKey(fileReference);
155
+ const cached = globalUrlCache.get(cacheKey);
156
+ const now = Date.now();
157
+
158
+ // Return cached URL if still valid
159
+ if (cached && cached.expiresAt > now) {
160
+ return cached.url;
161
+ }
162
+
163
+ // Generate new URL
164
+ let url: string | null = null;
165
+
166
+ try {
167
+ if (fileReference.is_public) {
168
+ // Public files: generate public URL (synchronous)
169
+ url = getPublicUrl(supabase, fileReference.file_path, true);
170
+ } else {
171
+ // Private files: generate signed URL (async)
172
+ const signedUrlResult = await getSignedUrl(supabase, fileReference.file_path, {
173
+ appName: 'pace-core',
174
+ orgId: organisationId,
175
+ expiresIn: Math.floor(ttl / 1000) // Convert ms to seconds
176
+ });
177
+ url = signedUrlResult?.url || null;
178
+ }
179
+
180
+ // Cache the URL if generated successfully
181
+ if (url) {
182
+ globalUrlCache.set(cacheKey, {
183
+ url,
184
+ expiresAt: now + ttl
185
+ });
186
+ cleanupCache(); // Clean up after adding
187
+ }
188
+
189
+ return url;
190
+ } catch (error) {
191
+ console.error('Failed to generate file URL:', error);
192
+ return null;
193
+ }
194
+ }, []);
195
+
196
+ const setUrl = useCallback((
197
+ fileReference: FileReference,
198
+ url: string,
199
+ ttl: number = DEFAULT_TTL_MS
200
+ ): void => {
201
+ const cacheKey = getCacheKey(fileReference);
202
+ globalUrlCache.set(cacheKey, {
203
+ url,
204
+ expiresAt: Date.now() + ttl
205
+ });
206
+ cleanupCache();
207
+ }, []);
208
+
209
+ const getCachedUrl = useCallback((fileReference: FileReference): string | null => {
210
+ const cacheKey = getCacheKey(fileReference);
211
+ const cached = globalUrlCache.get(cacheKey);
212
+ const now = Date.now();
213
+
214
+ if (cached && cached.expiresAt > now) {
215
+ return cached.url;
216
+ }
217
+
218
+ return null;
219
+ }, []);
220
+
221
+ const clearFile = useCallback((fileReference: FileReference): void => {
222
+ const cacheKey = getCacheKey(fileReference);
223
+ globalUrlCache.delete(cacheKey);
224
+ }, []);
225
+
226
+ const clearCache = useCallback((): void => {
227
+ globalUrlCache.clear();
228
+ }, []);
229
+
230
+ const getCacheStats = useCallback(() => {
231
+ return {
232
+ size: globalUrlCache.size,
233
+ maxSize: MAX_CACHE_SIZE
234
+ };
235
+ }, []);
236
+
237
+ return {
238
+ getUrl,
239
+ setUrl,
240
+ getCachedUrl,
241
+ clearFile,
242
+ clearCache,
243
+ getCacheStats
244
+ };
245
+ }
246
+
@@ -155,6 +155,7 @@ export function useInactivityTracker({
155
155
  const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
156
156
  const lastActivityRef = useRef<number>(Date.now());
157
157
  const channelRef = useRef<BroadcastChannel | null>(null);
158
+ const throttledResetActivityRef = useRef<((event: Event) => void) | null>(null);
158
159
 
159
160
  // Clear all timers
160
161
  const clearTimers = useCallback(() => {
@@ -294,47 +295,56 @@ export function useInactivityTracker({
294
295
  logger.warn('useInactivityTracker', 'Failed to check persisted activity time:', error);
295
296
  }
296
297
 
297
- // Set up throttled activity handler
298
- const throttledResetActivity = throttle((event) => {
299
- resetActivity();
300
- }, 100);
301
-
302
- // Add event listeners
303
- const addEventListeners = () => {
298
+ // Clean up any existing throttled handler and event listeners first
299
+ if (throttledResetActivityRef.current) {
304
300
  ACTIVITY_EVENTS.forEach(event => {
305
- document.addEventListener(event, throttledResetActivity, { passive: true });
301
+ document.removeEventListener(event, throttledResetActivityRef.current!);
306
302
  });
307
- };
303
+ }
308
304
 
309
- // Remove event listeners
310
- const removeEventListeners = () => {
311
- ACTIVITY_EVENTS.forEach(event => {
312
- document.removeEventListener(event, throttledResetActivity);
313
- });
314
- };
305
+ // Set up throttled activity handler - store in ref for proper cleanup
306
+ throttledResetActivityRef.current = throttle((event) => {
307
+ resetActivity();
308
+ }, 100);
315
309
 
316
- // Add listeners
317
- addEventListeners();
310
+ // Add event listeners
311
+ ACTIVITY_EVENTS.forEach(event => {
312
+ document.addEventListener(event, throttledResetActivityRef.current!, { passive: true });
313
+ });
318
314
 
319
315
  // Start the timer (skip activity callback for initial setup)
320
316
  resetActivity(true);
321
317
 
322
318
  // Cleanup function
323
319
  return () => {
324
- removeEventListeners();
320
+ // Remove event listeners using the stored ref
321
+ if (throttledResetActivityRef.current) {
322
+ ACTIVITY_EVENTS.forEach(event => {
323
+ document.removeEventListener(event, throttledResetActivityRef.current!);
324
+ });
325
+ throttledResetActivityRef.current = null;
326
+ }
325
327
  clearTimers();
326
328
  if (channelRef.current) {
327
329
  channelRef.current.close();
328
330
  channelRef.current = null;
329
331
  }
330
332
  };
331
- }, [enabled, isTracking, channelName, storageKey, idleTimeoutMs, warnBeforeMs, onIdle, onWarning]);
333
+ }, [enabled, isTracking, channelName, storageKey, idleTimeoutMs, warnBeforeMs, onIdle, onWarning, onActivity, resetActivity]);
332
334
 
333
335
  // Stop tracking
334
336
  const stopTracking = useCallback(() => {
335
337
  setIsTracking(false);
336
338
  clearTimers();
337
339
 
340
+ // Remove event listeners
341
+ if (throttledResetActivityRef.current) {
342
+ ACTIVITY_EVENTS.forEach(event => {
343
+ document.removeEventListener(event, throttledResetActivityRef.current!);
344
+ });
345
+ throttledResetActivityRef.current = null;
346
+ }
347
+
338
348
  if (channelRef.current) {
339
349
  channelRef.current.close();
340
350
  channelRef.current = null;
@@ -348,8 +358,9 @@ export function useInactivityTracker({
348
358
  return cleanup;
349
359
  } else {
350
360
  stopTracking();
361
+ return undefined;
351
362
  }
352
- }, [enabled, idleTimeoutMs, warnBeforeMs]);
363
+ }, [enabled, idleTimeoutMs, warnBeforeMs, startTracking, stopTracking]);
353
364
 
354
365
  // Cleanup on unmount
355
366
  useEffect(() => {
@@ -26,6 +26,7 @@ vi.mock('./useOrganisations', () => ({
26
26
  // Mock the RBAC API
27
27
  vi.mock('../rbac/api', () => ({
28
28
  isPermitted: vi.fn(),
29
+ isPermittedCached: vi.fn(),
29
30
  isSuperAdmin: vi.fn(),
30
31
  getPermissionMap: vi.fn()
31
32
  }));
@@ -35,13 +36,14 @@ vi.mock('../rbac/audit', () => ({
35
36
  emitAuditEvent: vi.fn()
36
37
  }));
37
38
 
38
- import { isPermitted, isSuperAdmin, getPermissionMap } from '../rbac/api';
39
+ import { isPermitted, isPermittedCached, isSuperAdmin, getPermissionMap } from '../rbac/api';
39
40
  import { emitAuditEvent } from '../rbac/audit';
40
41
 
41
42
  describe('useOrganisationSecurity', () => {
42
43
  const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
43
44
  const mockUseOrganisations = vi.mocked(useOrganisations);
44
45
  const mockIsPermitted = vi.mocked(isPermitted);
46
+ const mockIsPermittedCached = vi.mocked(isPermittedCached);
45
47
  const mockIsSuperAdmin = vi.mocked(isSuperAdmin);
46
48
  const mockGetPermissionMap = vi.mocked(getPermissionMap);
47
49
  const mockEmitAuditEvent = vi.mocked(emitAuditEvent);
@@ -99,6 +101,7 @@ describe('useOrganisationSecurity', () => {
99
101
  mockUseUnifiedAuth.mockClear();
100
102
  mockUseOrganisations.mockClear();
101
103
  mockIsPermitted.mockClear();
104
+ mockIsPermittedCached.mockClear();
102
105
  mockIsSuperAdmin.mockClear();
103
106
  mockGetPermissionMap.mockClear();
104
107
  mockEmitAuditEvent.mockClear();
@@ -508,13 +511,13 @@ describe('useOrganisationSecurity', () => {
508
511
  signOut: vi.fn(),
509
512
  } as any);
510
513
 
511
- mockIsPermitted.mockResolvedValue(true);
514
+ mockIsPermittedCached.mockResolvedValue(true);
512
515
 
513
516
  const { result } = renderHook(() => useOrganisationSecurity());
514
517
 
515
518
  const hasPermission = await result.current.hasPermission('read:users');
516
519
  expect(hasPermission).toBe(true);
517
- expect(mockIsPermitted).toHaveBeenCalledWith({
520
+ expect(mockIsPermittedCached).toHaveBeenCalledWith({
518
521
  userId: 'user-123',
519
522
  scope: {
520
523
  organisationId: 'org-123',
@@ -527,13 +530,13 @@ describe('useOrganisationSecurity', () => {
527
530
 
528
531
  it('checks permissions for specific organisation', async () => {
529
532
  mockIsSuperAdmin.mockResolvedValue(false);
530
- mockIsPermitted.mockResolvedValue(true);
533
+ mockIsPermittedCached.mockResolvedValue(true);
531
534
 
532
535
  const { result } = renderHook(() => useOrganisationSecurity());
533
536
 
534
537
  const hasPermission = await result.current.hasPermission('read:users', 'org-456');
535
538
  expect(hasPermission).toBe(true);
536
- expect(mockIsPermitted).toHaveBeenCalledWith({
539
+ expect(mockIsPermittedCached).toHaveBeenCalledWith({
537
540
  userId: 'user-123',
538
541
  scope: {
539
542
  organisationId: 'org-456',
@@ -546,7 +549,7 @@ describe('useOrganisationSecurity', () => {
546
549
 
547
550
  it('handles permission check errors gracefully', async () => {
548
551
  mockIsSuperAdmin.mockResolvedValue(false);
549
- mockIsPermitted.mockRejectedValue(new Error('Permission check failed'));
552
+ mockIsPermittedCached.mockRejectedValue(new Error('Permission check failed'));
550
553
 
551
554
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
552
555
 
@@ -622,7 +625,7 @@ describe('useOrganisationSecurity', () => {
622
625
 
623
626
  it('handles permission retrieval errors gracefully', async () => {
624
627
  mockIsSuperAdmin.mockResolvedValue(false);
625
- mockIsPermitted.mockRejectedValue(new Error('Permission retrieval failed'));
628
+ mockIsPermittedCached.mockRejectedValue(new Error('Permission retrieval failed'));
626
629
 
627
630
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
628
631
 
@@ -163,8 +163,8 @@ export const useOrganisationSecurity = (): OrganisationSecurityHook => {
163
163
  if (!targetOrgId || !user) return false;
164
164
 
165
165
  try {
166
- // Use the new RBAC system
167
- const { isPermitted } = await import('../rbac/api');
166
+ // Use the new RBAC system with caching
167
+ const { isPermittedCached } = await import('../rbac/api');
168
168
 
169
169
  const scope = {
170
170
  organisationId: targetOrgId,
@@ -172,7 +172,7 @@ export const useOrganisationSecurity = (): OrganisationSecurityHook => {
172
172
  appId: user.user_metadata?.appId || user.app_metadata?.appId,
173
173
  };
174
174
 
175
- return await isPermitted({
175
+ return await isPermittedCached({
176
176
  userId: user.id,
177
177
  scope,
178
178
  permission: permission as Permission
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @file usePreventTabReload Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks
5
+ * @since 0.6.0
6
+ *
7
+ * Prevents full page reloads when switching browser tabs.
8
+ * Handles browser back-forward cache (bfcache) restoration and tab visibility changes.
9
+ */
10
+
11
+ import { useEffect, useRef } from 'react';
12
+
13
+ export interface UsePreventTabReloadOptions {
14
+ /**
15
+ * Whether to enable the prevention logic
16
+ * @default true
17
+ */
18
+ enabled?: boolean;
19
+
20
+ /**
21
+ * Grace period in milliseconds to wait after tab becomes visible
22
+ * before allowing any potential reloads
23
+ * @default 2000
24
+ */
25
+ gracePeriodMs?: number;
26
+ }
27
+
28
+ /**
29
+ * Hook to prevent full page reloads when switching browser tabs.
30
+ *
31
+ * This hook handles:
32
+ * - Browser back-forward cache (bfcache) restoration
33
+ * - Tab visibility changes
34
+ * - Prevents unwanted page reloads when returning to a tab
35
+ *
36
+ * @param options - Configuration options
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * import { usePreventTabReload } from '@jmruthers/pace-core';
41
+ *
42
+ * function MyComponent() {
43
+ * usePreventTabReload();
44
+ * // Component code...
45
+ * }
46
+ * ```
47
+ */
48
+ export function usePreventTabReload(options: UsePreventTabReloadOptions = {}): void {
49
+ const { enabled = true, gracePeriodMs = 2000 } = options;
50
+ const isRestoringFromCacheRef = useRef(false);
51
+ const gracePeriodTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
52
+
53
+ useEffect(() => {
54
+ if (!enabled || typeof window === 'undefined') return;
55
+
56
+ // Handle pageshow event - detects when page is restored from bfcache
57
+ const handlePageShow = (event: PageTransitionEvent) => {
58
+ // If page was restored from bfcache, prevent any reload behavior
59
+ if (event.persisted) {
60
+ isRestoringFromCacheRef.current = true;
61
+
62
+ // Clear any existing timeout
63
+ if (gracePeriodTimeoutRef.current) {
64
+ clearTimeout(gracePeriodTimeoutRef.current);
65
+ }
66
+
67
+ // Set grace period to prevent reloads
68
+ gracePeriodTimeoutRef.current = setTimeout(() => {
69
+ isRestoringFromCacheRef.current = false;
70
+ }, gracePeriodMs);
71
+ }
72
+ };
73
+
74
+ // Handle visibility changes - when tab becomes visible
75
+ const handleVisibilityChange = () => {
76
+ if (!document.hidden) {
77
+ // Tab just became visible - set flag to prevent reloads
78
+ isRestoringFromCacheRef.current = true;
79
+
80
+ // Clear any existing timeout
81
+ if (gracePeriodTimeoutRef.current) {
82
+ clearTimeout(gracePeriodTimeoutRef.current);
83
+ }
84
+
85
+ // Set grace period
86
+ gracePeriodTimeoutRef.current = setTimeout(() => {
87
+ isRestoringFromCacheRef.current = false;
88
+ }, gracePeriodMs);
89
+ }
90
+ };
91
+
92
+ // Add event listeners
93
+ window.addEventListener('pageshow', handlePageShow);
94
+ document.addEventListener('visibilitychange', handleVisibilityChange);
95
+
96
+ return () => {
97
+ window.removeEventListener('pageshow', handlePageShow);
98
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
99
+
100
+ if (gracePeriodTimeoutRef.current) {
101
+ clearTimeout(gracePeriodTimeoutRef.current);
102
+ }
103
+ };
104
+ }, [enabled, gracePeriodMs]);
105
+ }
106
+