@jmruthers/pace-core 0.5.186 → 0.5.188

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 (290) hide show
  1. package/dist/{DataTable-IX2NBUTP.js → DataTable-GUFUNZ3N.js} +7 -7
  2. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  3. package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-643PUAIM.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-HGPQUCBC.js → chunk-2UUZZJFT.js} +3 -3
  8. package/dist/{chunk-445GEP27.js → chunk-3GOZZZYH.js} +33 -8
  9. package/dist/chunk-3GOZZZYH.js.map +1 -0
  10. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  11. package/dist/chunk-63FOKYGO.js.map +1 -0
  12. package/dist/{chunk-DAGICKHT.js → chunk-DDM4CCYT.js} +3 -3
  13. package/dist/{chunk-XAUHJD3L.js → chunk-E7UAOUMY.js} +2 -2
  14. package/dist/{chunk-HDCUMOOI.js → chunk-EFCLXK7F.js} +792 -559
  15. package/dist/chunk-EFCLXK7F.js.map +1 -0
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-GRIQLQ52.js → chunk-IM4QE42D.js} +27 -23
  19. package/dist/chunk-IM4QE42D.js.map +1 -0
  20. package/dist/{chunk-OALXJH4Y.js → chunk-IPCH26AG.js} +8 -8
  21. package/dist/chunk-IPCH26AG.js.map +1 -0
  22. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  23. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  24. package/dist/{chunk-TC7D3CR3.js → chunk-UNOTYLQF.js} +556 -101
  25. package/dist/chunk-UNOTYLQF.js.map +1 -0
  26. package/dist/{chunk-FXFJRTKI.js → chunk-VGZZXKBR.js} +5 -5
  27. package/dist/chunk-VGZZXKBR.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/getting-started/examples/README.md +2 -2
  208. package/docs/implementation-guides/file-upload-storage.md +29 -0
  209. package/docs/implementation-guides/public-pages.md +140 -1230
  210. package/docs/rbac/README.md +2 -1
  211. package/docs/rbac/api-reference.md +11 -0
  212. package/docs/rbac/performance.md +320 -0
  213. package/docs/standards/01-architecture-standard.md +5 -0
  214. package/docs/standards/05-security-standard.md +14 -0
  215. package/docs/standards/07-rbac-and-rls-standard.md +356 -0
  216. package/package.json +1 -1
  217. package/src/__tests__/public-recipe-view.test.ts +199 -0
  218. package/src/__tests__/rls-policies.test.ts +333 -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/index.ts +2 -0
  227. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  228. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  229. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  230. package/src/hooks/index.ts +6 -0
  231. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  232. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  233. package/src/hooks/useAddressAutocomplete.ts +268 -0
  234. package/src/hooks/useFileDisplay.ts +3 -15
  235. package/src/hooks/useFileReference.test.ts +20 -3
  236. package/src/hooks/useFileReference.ts +3 -24
  237. package/src/hooks/useFileUrlCache.ts +246 -0
  238. package/src/hooks/useInactivityTracker.ts +31 -20
  239. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  240. package/src/hooks/useOrganisationSecurity.ts +3 -3
  241. package/src/hooks/useQueryCache.ts +315 -0
  242. package/src/index.ts +2 -0
  243. package/src/providers/services/EventServiceProvider.tsx +4 -1
  244. package/src/rbac/api.test.ts +21 -6
  245. package/src/rbac/api.ts +32 -11
  246. package/src/rbac/audit-batched.ts +223 -0
  247. package/src/rbac/audit-enhanced.ts +2 -2
  248. package/src/rbac/audit.test.ts +6 -5
  249. package/src/rbac/audit.ts +34 -6
  250. package/src/rbac/cache-invalidation.ts +63 -12
  251. package/src/rbac/cache.test.ts +2 -2
  252. package/src/rbac/cache.ts +61 -14
  253. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  254. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  255. package/src/rbac/config.ts +9 -0
  256. package/src/rbac/engine.ts +2 -21
  257. package/src/rbac/hooks/usePermissions.ts +21 -5
  258. package/src/rbac/index.ts +19 -0
  259. package/src/rbac/performance.ts +210 -0
  260. package/src/rbac/request-deduplication.ts +87 -0
  261. package/src/rbac/utils/deep-equal.ts +93 -0
  262. package/src/services/OrganisationService.ts +5 -4
  263. package/src/types/file-reference.ts +0 -1
  264. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  265. package/src/utils/file-reference/index.ts +44 -15
  266. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  267. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  268. package/src/utils/google-places/index.ts +26 -0
  269. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  270. package/src/utils/google-places/types.ts +94 -0
  271. package/src/utils/index.ts +23 -0
  272. package/src/utils/request-deduplication.ts +165 -0
  273. package/src/utils/storage/helpers.ts +143 -4
  274. package/dist/chunk-445GEP27.js.map +0 -1
  275. package/dist/chunk-FMUCXFII.js +0 -76
  276. package/dist/chunk-FMUCXFII.js.map +0 -1
  277. package/dist/chunk-FSFQFJCU.js.map +0 -1
  278. package/dist/chunk-FXFJRTKI.js.map +0 -1
  279. package/dist/chunk-GRIQLQ52.js.map +0 -1
  280. package/dist/chunk-HDCUMOOI.js.map +0 -1
  281. package/dist/chunk-OALXJH4Y.js.map +0 -1
  282. package/dist/chunk-TC7D3CR3.js.map +0 -1
  283. package/dist/chunk-U6WNSFX5.js.map +0 -1
  284. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
  285. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
  286. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  287. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  288. /package/dist/{chunk-HGPQUCBC.js.map → chunk-2UUZZJFT.js.map} +0 -0
  289. /package/dist/{chunk-DAGICKHT.js.map → chunk-DDM4CCYT.js.map} +0 -0
  290. /package/dist/{chunk-XAUHJD3L.js.map → chunk-E7UAOUMY.js.map} +0 -0
@@ -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,315 @@
1
+ /**
2
+ * Query Result Caching Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/QueryCache
5
+ * @since 2.0.0
6
+ *
7
+ * Provides in-memory caching for frequently accessed data to eliminate duplicate queries.
8
+ * Useful for caching user profiles, app pages, and other relatively static data.
9
+ */
10
+
11
+ import { useCallback, useRef, useEffect } from 'react';
12
+ import { SupabaseClient } from '@supabase/supabase-js';
13
+ import { Database } from '../types/database';
14
+ import { createLogger } from '../utils/core/logger';
15
+
16
+ const log = createLogger('useQueryCache');
17
+
18
+ interface CachedQueryEntry<T> {
19
+ data: T;
20
+ expiresAt: number; // Unix timestamp
21
+ promise?: Promise<T>; // For in-flight requests
22
+ }
23
+
24
+ /**
25
+ * In-memory cache for query results
26
+ * Key format: `table:filterKey:filterValue`
27
+ */
28
+ const queryCache = new Map<string, CachedQueryEntry<any>>();
29
+
30
+ // Cleanup interval (every 5 minutes)
31
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
32
+ let cleanupTimer: ReturnType<typeof setInterval> | null = null;
33
+
34
+ function runCacheCleanup() {
35
+ const now = Date.now();
36
+ const expiredKeys: string[] = [];
37
+
38
+ queryCache.forEach((entry, key) => {
39
+ if (entry.expiresAt <= now) {
40
+ expiredKeys.push(key);
41
+ }
42
+ });
43
+
44
+ expiredKeys.forEach(key => {
45
+ queryCache.delete(key);
46
+ log.debug(`Removed expired query from cache: ${key}`);
47
+ });
48
+ }
49
+
50
+ // Initialize cleanup timer once
51
+ if (typeof window !== 'undefined' && !cleanupTimer) {
52
+ cleanupTimer = setInterval(runCacheCleanup, CLEANUP_INTERVAL_MS);
53
+ log.debug('Query cache cleanup initialized.');
54
+ }
55
+
56
+ export interface UseQueryCacheOptions {
57
+ /** Time to live in seconds (default: 300 = 5 minutes) */
58
+ ttl?: number;
59
+ /** Whether to enable caching (default: true) */
60
+ enabled?: boolean;
61
+ }
62
+
63
+ export interface UseQueryCacheReturn {
64
+ /** Get cached query result or fetch if not cached */
65
+ getCachedQuery: <T>(
66
+ table: string,
67
+ filterKey: string,
68
+ filterValue: string,
69
+ fetchFn: () => Promise<T>,
70
+ options?: UseQueryCacheOptions
71
+ ) => Promise<T>;
72
+ /** Invalidate a specific cached query */
73
+ invalidateQuery: (table: string, filterKey: string, filterValue: string) => void;
74
+ /** Clear all cached queries */
75
+ clearCache: () => void;
76
+ /** Get cache statistics */
77
+ getCacheStats: () => { size: number; keys: string[] };
78
+ }
79
+
80
+ /**
81
+ * Hook for query result caching
82
+ *
83
+ * Provides caching for frequently accessed data to eliminate duplicate queries.
84
+ * Automatically handles cache expiration and cleanup.
85
+ *
86
+ * @param supabase - Supabase client (optional, can be passed in fetchFn)
87
+ * @returns Query cache utilities
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * const { getCachedQuery } = useQueryCache(supabase);
92
+ *
93
+ * const person = await getCachedQuery(
94
+ * 'pace_person',
95
+ * 'user_id',
96
+ * userId,
97
+ * async () => {
98
+ * const { data } = await supabase
99
+ * .from('pace_person')
100
+ * .select('id, first_name, last_name, email')
101
+ * .eq('user_id', userId)
102
+ * .single();
103
+ * return data;
104
+ * },
105
+ * { ttl: 300 } // 5 minutes
106
+ * );
107
+ * ```
108
+ */
109
+ export function useQueryCache(supabase?: SupabaseClient<Database>): UseQueryCacheReturn {
110
+ const getCachedQuery = useCallback(async <T,>(
111
+ table: string,
112
+ filterKey: string,
113
+ filterValue: string,
114
+ fetchFn: () => Promise<T>,
115
+ options: UseQueryCacheOptions = {}
116
+ ): Promise<T> => {
117
+ const { ttl = 300, enabled = true } = options; // Default 5 minutes
118
+ const cacheKey = `${table}:${filterKey}:${filterValue}`;
119
+ const now = Date.now();
120
+
121
+ if (!enabled) {
122
+ return fetchFn();
123
+ }
124
+
125
+ // Check cache
126
+ const cached = queryCache.get(cacheKey);
127
+ if (cached) {
128
+ // If data is still valid, return it
129
+ if (cached.expiresAt > now && cached.data !== undefined) {
130
+ log.debug(`Cache hit for query: ${cacheKey}`);
131
+ return cached.data as T;
132
+ }
133
+
134
+ // If there's an in-flight request, wait for it
135
+ if (cached.promise) {
136
+ log.debug(`Waiting for in-flight request: ${cacheKey}`);
137
+ return cached.promise as Promise<T>;
138
+ }
139
+ }
140
+
141
+ // Fetch data
142
+ log.debug(`Cache miss for query: ${cacheKey}, fetching...`);
143
+ const fetchPromise = fetchFn();
144
+
145
+ // Store promise for in-flight request deduplication
146
+ queryCache.set(cacheKey, {
147
+ data: undefined as any,
148
+ expiresAt: now + (ttl * 1000),
149
+ promise: fetchPromise,
150
+ });
151
+
152
+ try {
153
+ const data = await fetchPromise;
154
+
155
+ // Update cache with actual data
156
+ queryCache.set(cacheKey, {
157
+ data,
158
+ expiresAt: now + (ttl * 1000),
159
+ });
160
+
161
+ log.debug(`Cached query result: ${cacheKey}, expires in ${ttl}s`);
162
+ return data;
163
+ } catch (error) {
164
+ // Remove failed request from cache
165
+ queryCache.delete(cacheKey);
166
+ log.error(`Query failed for ${cacheKey}:`, error);
167
+ throw error;
168
+ }
169
+ }, []);
170
+
171
+ const invalidateQuery = useCallback((table: string, filterKey: string, filterValue: string) => {
172
+ const cacheKey = `${table}:${filterKey}:${filterValue}`;
173
+ queryCache.delete(cacheKey);
174
+ log.debug(`Invalidated query cache: ${cacheKey}`);
175
+ }, []);
176
+
177
+ const clearCache = useCallback(() => {
178
+ queryCache.clear();
179
+ log.debug('Cleared all query cache entries.');
180
+ }, []);
181
+
182
+ const getCacheStats = useCallback(() => {
183
+ return {
184
+ size: queryCache.size,
185
+ keys: Array.from(queryCache.keys()),
186
+ };
187
+ }, []);
188
+
189
+ return {
190
+ getCachedQuery,
191
+ invalidateQuery,
192
+ clearCache,
193
+ getCacheStats,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Pre-configured cache helpers for common queries
199
+ */
200
+ export const queryCacheHelpers = {
201
+ /**
202
+ * Cache pace_person queries by user_id
203
+ * TTL: 5 minutes
204
+ */
205
+ pacePersonByUserId: <T>(
206
+ supabase: SupabaseClient<Database>,
207
+ userId: string,
208
+ fetchFn: () => Promise<T>
209
+ ): Promise<T> => {
210
+ const cacheKey = `pace_person:user_id:${userId}`;
211
+ const now = Date.now();
212
+ const ttl = 300 * 1000; // 5 minutes
213
+
214
+ const cached = queryCache.get(cacheKey);
215
+ if (cached && cached.expiresAt > now && cached.data !== undefined) {
216
+ return Promise.resolve(cached.data as T);
217
+ }
218
+
219
+ if (cached?.promise) {
220
+ return cached.promise as Promise<T>;
221
+ }
222
+
223
+ const promise = fetchFn();
224
+ queryCache.set(cacheKey, {
225
+ data: undefined as any,
226
+ expiresAt: now + ttl,
227
+ promise,
228
+ });
229
+
230
+ promise.then(data => {
231
+ queryCache.set(cacheKey, { data, expiresAt: now + ttl });
232
+ }).catch(() => {
233
+ queryCache.delete(cacheKey);
234
+ });
235
+
236
+ return promise;
237
+ },
238
+
239
+ /**
240
+ * Cache pace_member queries by person_id
241
+ * TTL: 5 minutes
242
+ */
243
+ paceMemberByPersonId: <T>(
244
+ supabase: SupabaseClient<Database>,
245
+ personId: string,
246
+ fetchFn: () => Promise<T>
247
+ ): Promise<T> => {
248
+ const cacheKey = `pace_member:person_id:${personId}`;
249
+ const now = Date.now();
250
+ const ttl = 300 * 1000; // 5 minutes
251
+
252
+ const cached = queryCache.get(cacheKey);
253
+ if (cached && cached.expiresAt > now && cached.data !== undefined) {
254
+ return Promise.resolve(cached.data as T);
255
+ }
256
+
257
+ if (cached?.promise) {
258
+ return cached.promise as Promise<T>;
259
+ }
260
+
261
+ const promise = fetchFn();
262
+ queryCache.set(cacheKey, {
263
+ data: undefined as any,
264
+ expiresAt: now + ttl,
265
+ promise,
266
+ });
267
+
268
+ promise.then(data => {
269
+ queryCache.set(cacheKey, { data, expiresAt: now + ttl });
270
+ }).catch(() => {
271
+ queryCache.delete(cacheKey);
272
+ });
273
+
274
+ return promise;
275
+ },
276
+
277
+ /**
278
+ * Cache rbac_app_pages queries by app_id
279
+ * TTL: 15 minutes (app pages are relatively static)
280
+ */
281
+ rbacAppPagesByAppId: <T>(
282
+ supabase: SupabaseClient<Database>,
283
+ appId: string,
284
+ fetchFn: () => Promise<T>
285
+ ): Promise<T> => {
286
+ const cacheKey = `rbac_app_pages:app_id:${appId}`;
287
+ const now = Date.now();
288
+ const ttl = 15 * 60 * 1000; // 15 minutes
289
+
290
+ const cached = queryCache.get(cacheKey);
291
+ if (cached && cached.expiresAt > now && cached.data !== undefined) {
292
+ return Promise.resolve(cached.data as T);
293
+ }
294
+
295
+ if (cached?.promise) {
296
+ return cached.promise as Promise<T>;
297
+ }
298
+
299
+ const promise = fetchFn();
300
+ queryCache.set(cacheKey, {
301
+ data: undefined as any,
302
+ expiresAt: now + ttl,
303
+ promise,
304
+ });
305
+
306
+ promise.then(data => {
307
+ queryCache.set(cacheKey, { data, expiresAt: now + ttl });
308
+ }).catch(() => {
309
+ queryCache.delete(cacheKey);
310
+ });
311
+
312
+ return promise;
313
+ },
314
+ };
315
+
package/src/index.ts CHANGED
@@ -76,6 +76,8 @@ export type { CardProps } from './components/Card/Card';
76
76
 
77
77
  export { Input } from './components/Input/Input';
78
78
  export type { InputProps } from './components/Input/Input';
79
+ export { AddressField } from './components/AddressField';
80
+ export type { AddressFieldProps, AddressFieldRef, ParsedAddress, AutocompleteOptions } from './components/AddressField';
79
81
  export { Label } from './components/Label/Label';
80
82
  export type { LabelProps } from './components/Label/Label';
81
83
 
@@ -50,6 +50,7 @@ export function EventServiceProvider({
50
50
  const eventService = eventServiceRef.current;
51
51
 
52
52
  // Update service dependencies and initialize when dependencies change
53
+ // Note: eventService is a ref and never changes, so we don't include it in dependencies
53
54
  useEffect(() => {
54
55
  let isMounted = true;
55
56
 
@@ -72,7 +73,9 @@ export function EventServiceProvider({
72
73
  return () => {
73
74
  isMounted = false;
74
75
  };
75
- }, [eventService, supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId]);
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ // eventService is a ref and never changes, so we exclude it from dependencies
78
+ }, [supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId]);
76
79
 
77
80
  // Cleanup service on unmount only
78
81
  useEffect(() => {
@@ -45,6 +45,12 @@ vi.mock('./cache', () => ({
45
45
  }
46
46
  }));
47
47
 
48
+ vi.mock('./request-deduplication', () => ({
49
+ getOrCreateRequest: vi.fn((input, checkFn) => checkFn(input)),
50
+ clearInFlightRequests: vi.fn(),
51
+ getInFlightRequestCount: vi.fn(() => 0),
52
+ }));
53
+
48
54
  vi.mock('./config', () => ({
49
55
  createRBACConfig: vi.fn(),
50
56
  getRBACLogger: vi.fn(() => ({
@@ -120,7 +126,14 @@ describe('RBAC API', () => {
120
126
  process.env.NODE_ENV = originalEnv;
121
127
 
122
128
  expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
123
- expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
129
+ expect(mockCreateAuditManager).toHaveBeenCalledWith(
130
+ mockSupabase,
131
+ true,
132
+ expect.objectContaining({
133
+ batchSize: undefined,
134
+ batchWindow: undefined,
135
+ })
136
+ );
124
137
  expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
125
138
  expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
126
139
  });
@@ -659,7 +672,7 @@ describe('RBAC API', () => {
659
672
  });
660
673
 
661
674
  expect(result).toBe(true);
662
- expect(rbacCache.get).toHaveBeenCalledWith(cacheKey);
675
+ expect(rbacCache.get).toHaveBeenCalledWith(cacheKey, true);
663
676
  expect(mockEngine.isPermitted).not.toHaveBeenCalled();
664
677
  });
665
678
 
@@ -689,10 +702,12 @@ describe('RBAC API', () => {
689
702
  organisationId: 'org-456'
690
703
  })
691
704
  );
692
- expect(rbacCache.set).toHaveBeenCalledWith(
693
- expect.any(String),
694
- true
695
- );
705
+ // Check that cache.set was called - pageId presence makes it a page-level check
706
+ expect(rbacCache.set).toHaveBeenCalled();
707
+ const setCall = rbacCache.set.mock.calls[0];
708
+ expect(setCall[0]).toBeTruthy(); // cache key
709
+ expect(setCall[1]).toBe(true); // result
710
+ // setCall[2] is TTL (optional), setCall[3] is useSessionCache (true for page-level)
696
711
  });
697
712
  });
698
713
 
package/src/rbac/api.ts CHANGED
@@ -27,6 +27,8 @@ import { rbacCache, RBACCache, CACHE_PATTERNS } from './cache';
27
27
  import { createRBACConfig, RBACConfig, getRBACLogger } from './config';
28
28
  import { SecurityContext } from './security';
29
29
  import { createLogger } from '../utils/core/logger';
30
+ import { enablePerformanceMonitoring } from './performance';
31
+ import { getOrCreateRequest } from './request-deduplication';
30
32
 
31
33
  const log = createLogger('RBACAPI');
32
34
 
@@ -72,10 +74,20 @@ export function setupRBAC(supabase: SupabaseClient<Database>, config?: Partial<R
72
74
  // Pass security config to engine
73
75
  globalEngine = createRBACEngine(supabase, securityConfig);
74
76
 
75
- // Setup audit manager
76
- const auditManager = createAuditManager(supabase);
77
+ // Setup audit manager with batching configuration
78
+ const useBatchedAudit = config?.audit?.batched !== false && (config?.performance?.enableBatchedAuditLogging !== false);
79
+ const batchConfig = useBatchedAudit ? {
80
+ batchWindow: config?.audit?.batchWindow,
81
+ batchSize: config?.audit?.batchSize,
82
+ } : undefined;
83
+ const auditManager = createAuditManager(supabase, useBatchedAudit, batchConfig);
77
84
  setGlobalAuditManager(auditManager);
78
85
 
86
+ // Setup performance monitoring if enabled
87
+ if (config?.performance?.enablePerformanceTracking) {
88
+ enablePerformanceMonitoring();
89
+ }
90
+
79
91
  logger.info('RBAC system initialized successfully');
80
92
  }
81
93
 
@@ -195,13 +207,16 @@ export async function isPermitted(input: PermissionCheck): Promise<boolean> {
195
207
  /**
196
208
  * Check if user has a specific permission (cached version)
197
209
  *
210
+ * Uses request deduplication to share in-flight requests across components
211
+ * and checks cache before making new requests. Uses session cache for page-level checks.
212
+ *
198
213
  * @param input - Permission check input
199
214
  * @returns Promise resolving to permission result
200
215
  */
201
216
  export async function isPermittedCached(input: PermissionCheck): Promise<boolean> {
202
217
  const { userId, scope, permission, pageId } = input;
203
218
 
204
- // Check cache first
219
+ // Check cache first (checks both short-term and session cache)
205
220
  const cacheKey = RBACCache.generatePermissionKey({
206
221
  userId,
207
222
  organisationId: scope.organisationId!,
@@ -211,18 +226,24 @@ export async function isPermittedCached(input: PermissionCheck): Promise<boolean
211
226
  pageId,
212
227
  });
213
228
 
214
- const cached = rbacCache.get<boolean>(cacheKey);
229
+ const cached = rbacCache.get<boolean>(cacheKey, true);
215
230
  if (cached !== null) {
216
231
  return cached;
217
232
  }
218
233
 
219
- // Check permission
220
- const result = await isPermitted(input);
221
-
222
- // Cache result
223
- rbacCache.set(cacheKey, result);
224
-
225
- return result;
234
+ // Use request deduplication - if same request is in-flight, share the promise
235
+ return getOrCreateRequest(input, async (checkInput) => {
236
+ // Check permission
237
+ const result = await isPermitted(checkInput);
238
+
239
+ // Determine if this is a page-level check (has pageId or permission contains 'page.')
240
+ const isPageLevelCheck = !!pageId || permission.includes('page.');
241
+
242
+ // Cache result - use session cache for page-level checks
243
+ rbacCache.set(cacheKey, result, undefined, isPageLevelCheck);
244
+
245
+ return result;
246
+ });
226
247
  }
227
248
 
228
249
  /**