@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.
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/chunk-SAUPYVLF.js.map +1 -0
- package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
- package/dist/chunk-ULX5FYEM.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
- package/dist/hooks.d.ts +265 -6
- package/dist/hooks.js +148 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +25 -10
- package/dist/index.js +65 -30
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +125 -8
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
- package/dist/utils.d.ts +214 -4
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +6 -6
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +33 -9
- package/docs/api/interfaces/FileUploadProps.md +36 -14
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +27 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +328 -69
- package/docs/api-reference/components.md +26 -12
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-reference-system.md +24 -2
- package/docs/implementation-guides/file-upload-storage.md +38 -1
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.js +512 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +7 -1
- package/src/components/Header/Header.tsx +2 -5
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +9 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +21 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +3 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
- package/src/utils/file-reference/index.ts +56 -17
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +211 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-AISXLWGZ.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-HC67NW5K.js.map +0 -1
- package/dist/chunk-IXSNYUCT.js.map +0 -1
- package/dist/chunk-MX3EIJGQ.js.map +0 -1
- package/dist/chunk-OKI34GZD.js.map +0 -1
- package/dist/chunk-STTZQK2I.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /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 =
|
|
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
|
-
//
|
|
298
|
-
|
|
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.
|
|
301
|
+
document.removeEventListener(event, throttledResetActivityRef.current!);
|
|
306
302
|
});
|
|
307
|
-
}
|
|
303
|
+
}
|
|
308
304
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
+
|