@jmruthers/pace-core 0.5.186 → 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-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
- 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-OALXJH4Y.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-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
- 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-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
- package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
- 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/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
- package/dist/hooks.d.ts +221 -6
- package/dist/hooks.js +146 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.js +62 -28
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +124 -7
- 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 +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
- package/dist/utils.d.ts +213 -3
- 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 +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- 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 +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- 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 +1 -1
- 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 +26 -3
- 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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- 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 +318 -59
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-upload-storage.md +29 -0
- 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/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/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 +6 -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 +20 -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/useQueryCache.ts +315 -0
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- 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/types/file-reference.ts +0 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
- package/src/utils/file-reference/index.ts +44 -15
- 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/storage/helpers.ts +143 -4
- package/dist/chunk-445GEP27.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-GRIQLQ52.js.map +0 -1
- package/dist/chunk-HDCUMOOI.js.map +0 -1
- package/dist/chunk-OALXJH4Y.js.map +0 -1
- package/dist/chunk-TC7D3CR3.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
- /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Address Autocomplete Hook
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/AddressAutocomplete
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Hook for managing Google Places API autocomplete functionality.
|
|
8
|
+
* Provides debounced input, caching, and address selection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
12
|
+
import { useDebounce } from './useDebounce';
|
|
13
|
+
import { useQueryCache } from './useQueryCache';
|
|
14
|
+
import {
|
|
15
|
+
fetchPlaceAutocomplete,
|
|
16
|
+
fetchPlaceDetails,
|
|
17
|
+
createAddressFromPlaceResult,
|
|
18
|
+
getAddressByPlaceId,
|
|
19
|
+
} from '../utils/google-places';
|
|
20
|
+
import type {
|
|
21
|
+
GooglePlaceAutocompletePrediction,
|
|
22
|
+
ParsedAddress,
|
|
23
|
+
AutocompleteOptions,
|
|
24
|
+
} from '../utils/google-places';
|
|
25
|
+
|
|
26
|
+
export interface UseAddressAutocompleteOptions {
|
|
27
|
+
/** Google Places API options */
|
|
28
|
+
autocompleteOptions?: AutocompleteOptions;
|
|
29
|
+
/** Debounce delay in milliseconds (default: 300) */
|
|
30
|
+
debounceDelay?: number;
|
|
31
|
+
/** Enable caching (default: true) */
|
|
32
|
+
cacheEnabled?: boolean;
|
|
33
|
+
/** Cache TTL configuration */
|
|
34
|
+
cacheTTL?: {
|
|
35
|
+
/** Autocomplete cache TTL in seconds (default: 3600 = 1 hour) */
|
|
36
|
+
autocomplete?: number;
|
|
37
|
+
/** Place details cache TTL in seconds (default: 86400 = 24 hours) */
|
|
38
|
+
placeDetails?: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseAddressAutocompleteReturn {
|
|
43
|
+
/** Array of autocomplete suggestions */
|
|
44
|
+
suggestions: GooglePlaceAutocompletePrediction[];
|
|
45
|
+
/** Loading state */
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
/** Error state */
|
|
48
|
+
error: Error | null;
|
|
49
|
+
/** Select an address by place_id */
|
|
50
|
+
selectAddress: (placeId: string) => Promise<ParsedAddress | null>;
|
|
51
|
+
/** Get address by place_id (uses cache) */
|
|
52
|
+
getAddressByPlaceId: (placeId: string) => Promise<ParsedAddress | null>;
|
|
53
|
+
/** Clear suggestions */
|
|
54
|
+
clearSuggestions: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hook for Google Places API autocomplete with caching
|
|
59
|
+
*
|
|
60
|
+
* @param apiKey - Google Places API key
|
|
61
|
+
* @param inputValue - Current input value
|
|
62
|
+
* @param options - Hook configuration options
|
|
63
|
+
* @returns Autocomplete state and functions
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const { suggestions, isLoading, selectAddress } = useAddressAutocomplete(
|
|
68
|
+
* apiKey,
|
|
69
|
+
* inputValue,
|
|
70
|
+
* { debounceDelay: 300, cacheEnabled: true }
|
|
71
|
+
* );
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useAddressAutocomplete(
|
|
75
|
+
apiKey: string,
|
|
76
|
+
inputValue: string,
|
|
77
|
+
options: UseAddressAutocompleteOptions = {}
|
|
78
|
+
): UseAddressAutocompleteReturn {
|
|
79
|
+
const {
|
|
80
|
+
debounceDelay = 300,
|
|
81
|
+
cacheEnabled = true,
|
|
82
|
+
cacheTTL = {
|
|
83
|
+
autocomplete: 3600, // 1 hour
|
|
84
|
+
placeDetails: 86400, // 24 hours
|
|
85
|
+
},
|
|
86
|
+
autocompleteOptions,
|
|
87
|
+
} = options;
|
|
88
|
+
|
|
89
|
+
const [suggestions, setSuggestions] = useState<GooglePlaceAutocompletePrediction[]>([]);
|
|
90
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
91
|
+
const [error, setError] = useState<Error | null>(null);
|
|
92
|
+
|
|
93
|
+
const debouncedInput = useDebounce(inputValue, debounceDelay);
|
|
94
|
+
const { getCachedQuery } = useQueryCache();
|
|
95
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
96
|
+
|
|
97
|
+
// Memoize autocompleteOptions to prevent unnecessary re-renders
|
|
98
|
+
const memoizedAutocompleteOptions = useMemo(
|
|
99
|
+
() => autocompleteOptions,
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
[JSON.stringify(autocompleteOptions)]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Fetch autocomplete suggestions
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
// Cancel previous request if still in flight
|
|
107
|
+
if (abortControllerRef.current) {
|
|
108
|
+
abortControllerRef.current.abort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reset state if input is empty
|
|
112
|
+
if (!debouncedInput.trim()) {
|
|
113
|
+
setSuggestions([]);
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
setError(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Don't fetch if no API key
|
|
120
|
+
if (!apiKey) {
|
|
121
|
+
setError(new Error('Google Places API key is required'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setIsLoading(true);
|
|
126
|
+
setError(null);
|
|
127
|
+
|
|
128
|
+
const fetchSuggestions = async () => {
|
|
129
|
+
try {
|
|
130
|
+
let predictions: GooglePlaceAutocompletePrediction[];
|
|
131
|
+
|
|
132
|
+
if (cacheEnabled) {
|
|
133
|
+
// Use cached query with TTL
|
|
134
|
+
predictions = await getCachedQuery(
|
|
135
|
+
'google-places-autocomplete',
|
|
136
|
+
'query',
|
|
137
|
+
debouncedInput,
|
|
138
|
+
async () => {
|
|
139
|
+
return fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
|
|
140
|
+
},
|
|
141
|
+
{ ttl: cacheTTL.autocomplete, enabled: true }
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
// Direct fetch without cache
|
|
145
|
+
predictions = await fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setSuggestions(predictions);
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Ignore aborted requests
|
|
152
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch autocomplete suggestions');
|
|
157
|
+
setError(error);
|
|
158
|
+
setSuggestions([]);
|
|
159
|
+
setIsLoading(false);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
fetchSuggestions();
|
|
164
|
+
|
|
165
|
+
// Cleanup function to cancel in-flight requests
|
|
166
|
+
return () => {
|
|
167
|
+
if (abortControllerRef.current) {
|
|
168
|
+
abortControllerRef.current.abort();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, [debouncedInput, apiKey, cacheEnabled, cacheTTL.autocomplete]);
|
|
173
|
+
|
|
174
|
+
// Select address by place_id
|
|
175
|
+
const selectAddress = useCallback(
|
|
176
|
+
async (placeId: string): Promise<ParsedAddress | null> => {
|
|
177
|
+
if (!placeId || !apiKey) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setIsLoading(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
let placeDetails;
|
|
186
|
+
|
|
187
|
+
if (cacheEnabled) {
|
|
188
|
+
// Use cached query with TTL
|
|
189
|
+
placeDetails = await getCachedQuery(
|
|
190
|
+
'google-places-details',
|
|
191
|
+
'place_id',
|
|
192
|
+
placeId,
|
|
193
|
+
async () => {
|
|
194
|
+
return fetchPlaceDetails(placeId, apiKey);
|
|
195
|
+
},
|
|
196
|
+
{ ttl: cacheTTL.placeDetails, enabled: true }
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
// Direct fetch without cache
|
|
200
|
+
placeDetails = await fetchPlaceDetails(placeId, apiKey);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parsedAddress = createAddressFromPlaceResult(placeDetails);
|
|
204
|
+
setIsLoading(false);
|
|
205
|
+
return parsedAddress;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const error = err instanceof Error ? err : new Error('Failed to fetch place details');
|
|
208
|
+
setError(error);
|
|
209
|
+
setIsLoading(false);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
+
[apiKey, cacheEnabled, cacheTTL.placeDetails]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Get address by place_id (utility function)
|
|
218
|
+
const getAddressByPlaceIdFn = useCallback(
|
|
219
|
+
async (placeId: string): Promise<ParsedAddress | null> => {
|
|
220
|
+
if (!placeId || !apiKey) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (cacheEnabled) {
|
|
226
|
+
// Use cached query with TTL
|
|
227
|
+
return await getCachedQuery(
|
|
228
|
+
'google-places-details',
|
|
229
|
+
'place_id',
|
|
230
|
+
placeId,
|
|
231
|
+
async () => {
|
|
232
|
+
const result = await getAddressByPlaceId(placeId, apiKey);
|
|
233
|
+
if (!result) {
|
|
234
|
+
throw new Error('Failed to fetch address');
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
},
|
|
238
|
+
{ ttl: cacheTTL.placeDetails, enabled: true }
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
return await getAddressByPlaceId(placeId, apiKey);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const error = err instanceof Error ? err : new Error('Failed to get address by place_id');
|
|
245
|
+
setError(error);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
+
[apiKey, cacheEnabled, cacheTTL.placeDetails]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Clear suggestions
|
|
254
|
+
const clearSuggestions = useCallback(() => {
|
|
255
|
+
setSuggestions([]);
|
|
256
|
+
setError(null);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
suggestions,
|
|
261
|
+
isLoading,
|
|
262
|
+
error,
|
|
263
|
+
selectAddress,
|
|
264
|
+
getAddressByPlaceId: getAddressByPlaceIdFn,
|
|
265
|
+
clearSuggestions,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import { useState, useEffect, useCallback } from 'react';
|
|
38
38
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
39
39
|
import { FileReference, FileCategory } from '../types/file-reference';
|
|
40
|
-
import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
|
|
40
|
+
import { getPublicUrl, getSignedUrl, generateFileUrlsBatch } from '../utils/storage/helpers';
|
|
41
41
|
import { createFileReferenceService } from '../utils/file-reference';
|
|
42
42
|
import { logger } from '../utils/core/logger';
|
|
43
43
|
|
|
@@ -279,24 +279,12 @@ export function useFileDisplay(
|
|
|
279
279
|
logger.debug('useFileDisplay', 'Setting file URL:', url ? 'URL set' : 'URL is null');
|
|
280
280
|
setFileUrl(url);
|
|
281
281
|
} else {
|
|
282
|
-
// Multiple files mode - generate URLs for all files
|
|
283
|
-
const urlMap =
|
|
284
|
-
for (const fileRef of files) {
|
|
285
|
-
let url: string | null = null;
|
|
286
|
-
if (fileRef.is_public) {
|
|
287
|
-
url = getPublicUrl(supabase, fileRef.file_path, true);
|
|
288
|
-
} else {
|
|
289
|
-
const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
|
|
282
|
+
// Multiple files mode - generate URLs for all files in batch
|
|
283
|
+
const urlMap = await generateFileUrlsBatch(supabase, files, {
|
|
290
284
|
appName: 'pace-core',
|
|
291
285
|
orgId: organisation_id,
|
|
292
286
|
expiresIn: 3600
|
|
293
287
|
});
|
|
294
|
-
url = signedUrlResult?.url || null;
|
|
295
|
-
}
|
|
296
|
-
if (url) {
|
|
297
|
-
urlMap.set(fileRef.id, url);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
288
|
setFileUrls(urlMap);
|
|
301
289
|
setFileReference(null);
|
|
302
290
|
setFileUrl(null);
|
|
@@ -18,6 +18,7 @@ let serviceMock: any;
|
|
|
18
18
|
const mockUploadFileWithReference = vi.fn();
|
|
19
19
|
const mockGetPublicUrl = vi.fn();
|
|
20
20
|
const mockGetSignedUrl = vi.fn();
|
|
21
|
+
const mockGenerateFileUrlsBatch = vi.fn();
|
|
21
22
|
|
|
22
23
|
// Setup mocks
|
|
23
24
|
beforeEach(() => {
|
|
@@ -47,12 +48,25 @@ beforeEach(() => {
|
|
|
47
48
|
url: 'https://example.com/signed-file.jpg',
|
|
48
49
|
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString()
|
|
49
50
|
});
|
|
51
|
+
// Mock batch URL generation to return URLs for all files
|
|
52
|
+
mockGenerateFileUrlsBatch.mockImplementation(async (supabase, files) => {
|
|
53
|
+
const urlMap = new Map<string, string>();
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
if (file.is_public) {
|
|
56
|
+
urlMap.set(file.id, `https://example.com/${file.file_path}`);
|
|
57
|
+
} else {
|
|
58
|
+
urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return urlMap;
|
|
62
|
+
});
|
|
50
63
|
|
|
51
64
|
// Apply mocks
|
|
52
65
|
vi.spyOn(fileReferenceUtils, 'createFileReferenceService').mockImplementation(mockCreateFileReferenceService as any);
|
|
53
66
|
vi.spyOn(fileReferenceUtils, 'uploadFileWithReference').mockImplementation(mockUploadFileWithReference as any);
|
|
54
67
|
vi.spyOn(storageHelpers, 'getPublicUrl').mockImplementation(mockGetPublicUrl as any);
|
|
55
68
|
vi.spyOn(storageHelpers, 'getSignedUrl').mockImplementation(mockGetSignedUrl as any);
|
|
69
|
+
vi.spyOn(storageHelpers, 'generateFileUrlsBatch').mockImplementation(mockGenerateFileUrlsBatch as any);
|
|
56
70
|
});
|
|
57
71
|
|
|
58
72
|
afterEach(() => {
|
|
@@ -505,16 +519,19 @@ describe('[hook] useFilesByCategory', () => {
|
|
|
505
519
|
);
|
|
506
520
|
|
|
507
521
|
await waitFor(() => {
|
|
508
|
-
|
|
509
|
-
expect(
|
|
522
|
+
// Verify batch URL generation was called with both files
|
|
523
|
+
expect(mockGenerateFileUrlsBatch).toHaveBeenCalledWith(
|
|
510
524
|
mockSupabase,
|
|
511
|
-
privateFile
|
|
525
|
+
[publicFile, privateFile],
|
|
512
526
|
expect.objectContaining({
|
|
513
527
|
appName: 'file-reference',
|
|
514
528
|
orgId: 'test-org-123',
|
|
515
529
|
expiresIn: 3600
|
|
516
530
|
})
|
|
517
531
|
);
|
|
532
|
+
// Verify URLs were generated
|
|
533
|
+
expect(result.current.fileUrls.get(publicFile.id)).toBeDefined();
|
|
534
|
+
expect(result.current.fileUrls.get(privateFile.id)).toBeDefined();
|
|
518
535
|
});
|
|
519
536
|
});
|
|
520
537
|
});
|
|
@@ -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
|
+
|