diff --git a/src/hash.test.ts b/src/hash.test.ts new file mode 100644 index 00000000..e3a9c97c --- /dev/null +++ b/src/hash.test.ts @@ -0,0 +1,22 @@ +import {hash} from './hash'; + +describe('hash', () => { + it('should generate a hash from a string', () => { + const result = hash('foo'); + + expect(result).toEqual('18cc6'); + expect(result).toEqual(hash('foo')); + }); + + it('should handle special characters', () => { + expect(hash('✨')).toEqual('2728'); + expect(hash('💥')).toEqual('d83d'); + expect(hash('✨💥')).toEqual('59615'); + }); + + it('should generate a hash from an empty string', () => { + const result = hash(''); + + expect(result).toEqual('0'); + }); +}); diff --git a/src/hash.ts b/src/hash.ts new file mode 100644 index 00000000..4e14e347 --- /dev/null +++ b/src/hash.ts @@ -0,0 +1,12 @@ +export function hash(value: string): string { + let code = 0; + + for (const char of value) { + const charCode = char.charCodeAt(0); + + code = (code << 5) - code + charCode; + code |= 0; // Convert to 32bit integer + } + + return code.toString(16); +} diff --git a/src/hooks/useContent.test.ts b/src/hooks/useContent.test.ts index 2266a468..ea6e33ce 100644 --- a/src/hooks/useContent.test.ts +++ b/src/hooks/useContent.test.ts @@ -3,6 +3,7 @@ import {Plug} from '@croct/plug'; import {useCroct} from './useCroct'; import {useLoader} from './useLoader'; import {useContent} from './useContent'; +import {hash} from '../hash'; jest.mock( './useCroct', @@ -30,11 +31,15 @@ describe('useContent (CSR)', () => { jest.mocked(useLoader).mockReturnValue('foo'); const slotId = 'home-banner@1'; + const preferredLocale = 'en'; + const attributes = {example: 'value'}; + const cacheKey = 'unique'; const {result} = renderHook( () => useContent<{title: string}>(slotId, { - preferredLocale: 'en', - cacheKey: 'unique', + preferredLocale: preferredLocale, + attributes: attributes, + cacheKey: cacheKey, fallback: { title: 'error', }, @@ -44,7 +49,7 @@ describe('useContent (CSR)', () => { expect(useCroct).toHaveBeenCalled(); expect(useLoader).toHaveBeenCalledWith({ - cacheKey: `useContent:unique:${slotId}`, + cacheKey: hash(`useContent:${cacheKey}:${slotId}:${preferredLocale}:${JSON.stringify(attributes)}`), fallback: { title: 'error', }, @@ -59,6 +64,7 @@ describe('useContent (CSR)', () => { expect(fetch).toHaveBeenCalledWith(slotId, { preferredLocale: 'en', + attributes: attributes, }); expect(result.current).toBe('foo'); diff --git a/src/hooks/useContent.ts b/src/hooks/useContent.ts index fce1b5e6..887bf1a1 100644 --- a/src/hooks/useContent.ts +++ b/src/hooks/useContent.ts @@ -4,6 +4,7 @@ import {FetchOptions} from '@croct/plug/plug'; import {useLoader} from './useLoader'; import {useCroct} from './useCroct'; import {isSsr} from '../ssr-polyfills'; +import {hash} from '../hash'; export type UseContentOptions = FetchOptions & { fallback?: F, @@ -17,10 +18,16 @@ function useCsrContent( options: UseContentOptions = {}, ): SlotContent | I | F { const {fallback, initial, cacheKey, expiration, ...fetchOptions} = options; + const {preferredLocale} = fetchOptions; const croct = useCroct(); return useLoader({ - cacheKey: `useContent:${cacheKey ?? ''}:${id}`, + cacheKey: hash( + `useContent:${cacheKey ?? ''}` + + `:${id}` + + `:${preferredLocale ?? ''}` + + `:${JSON.stringify(fetchOptions.attributes ?? '')}`, + ), loader: () => croct.fetch(id, fetchOptions).then(({content}) => content), initial: initial, fallback: fallback, diff --git a/src/hooks/useEvaluation.test.ts b/src/hooks/useEvaluation.test.ts index a7f9ff7d..01fc1341 100644 --- a/src/hooks/useEvaluation.test.ts +++ b/src/hooks/useEvaluation.test.ts @@ -4,6 +4,7 @@ import {Plug} from '@croct/plug'; import {useEvaluation} from './useEvaluation'; import {useCroct} from './useCroct'; import {useLoader} from './useLoader'; +import {hash} from '../hash'; jest.mock( './useCroct', @@ -39,11 +40,12 @@ describe('useEvaluation', () => { jest.mocked(useLoader).mockReturnValue('foo'); const query = 'location'; + const cacheKey = 'unique'; const {result} = renderHook( () => useEvaluation(query, { ...evaluationOptions, - cacheKey: 'unique', + cacheKey: cacheKey, fallback: 'error', expiration: 50, }), @@ -51,7 +53,7 @@ describe('useEvaluation', () => { expect(useCroct).toHaveBeenCalled(); expect(useLoader).toHaveBeenCalledWith({ - cacheKey: 'useEvaluation:unique:location:{"foo":"bar"}', + cacheKey: hash(`useEvaluation:${cacheKey}:${query}:${JSON.stringify(evaluationOptions.attributes)}`), fallback: 'error', expiration: 50, loader: expect.any(Function), diff --git a/src/hooks/useEvaluation.ts b/src/hooks/useEvaluation.ts index 382e8bb5..79f82fe7 100644 --- a/src/hooks/useEvaluation.ts +++ b/src/hooks/useEvaluation.ts @@ -3,6 +3,7 @@ import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade'; import {useLoader} from './useLoader'; import {useCroct} from './useCroct'; import {isSsr} from '../ssr-polyfills'; +import {hash} from '../hash'; function cleanEvaluationOptions(options: EvaluationOptions): EvaluationOptions { const result: EvaluationOptions = {}; @@ -36,7 +37,11 @@ function useCsrEvaluation( const croct = useCroct(); return useLoader({ - cacheKey: `useEvaluation:${cacheKey ?? ''}:${query}:${JSON.stringify(options.attributes ?? '')}`, + cacheKey: hash( + `useEvaluation:${cacheKey ?? ''}` + + `:${query}` + + `:${JSON.stringify(options.attributes ?? '')}`, + ), loader: () => croct.evaluate(query, cleanEvaluationOptions(evaluationOptions)), initial: initial, fallback: fallback,