-
Notifications
You must be signed in to change notification settings - Fork 497
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(instantsearch): hydrate search client (#5854)
* feat(react-instantsearch): hydrate search client * move function call in instantsearch.js * test: `hydrateSearchClient` to make sure it caches results (#5913) * test: add tests for `hydrateSearchClient` function * add test in InstantSearchSSRProvider * update bundlesize --------- Co-authored-by: Aymeric Giraudet <aymeric.giraudet@algolia.com>
- Loading branch information
1 parent
1501fae
commit 5b96771
Showing
8 changed files
with
304 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
packages/instantsearch.js/src/lib/utils/__tests__/hydrateSearchClient-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { hydrateSearchClient } from '../hydrateSearchClient'; | ||
|
||
import type { SearchClient, InitialResults } from '../../../types'; | ||
|
||
describe('hydrateSearchClient', () => { | ||
let client: SearchClient & { | ||
_cacheHydrated?: boolean; | ||
_useCache?: boolean; | ||
cache?: Record<string, string>; | ||
}; | ||
const initialResults = { | ||
instant_search: { | ||
results: [{ index: 'instant_search', params: 'params', nbHits: 1000 }], | ||
state: {}, | ||
rawResults: [{ index: 'instant_search', params: 'params', nbHits: 1000 }], | ||
}, | ||
} as unknown as InitialResults; | ||
|
||
it('should not hydrate the client if no results are provided', () => { | ||
client = {} as unknown as SearchClient; | ||
hydrateSearchClient(client, undefined); | ||
|
||
expect(client._cacheHydrated).not.toBeDefined(); | ||
}); | ||
|
||
it('should not hydrate the client if the cache is disabled', () => { | ||
client = { _useCache: false } as unknown as SearchClient; | ||
|
||
hydrateSearchClient(client, initialResults); | ||
|
||
expect(client._cacheHydrated).not.toBeDefined(); | ||
}); | ||
|
||
it('should not hydrate the client if `addAlgoliaAgent` is missing', () => { | ||
client = { addAlgoliaAgent: undefined } as unknown as SearchClient; | ||
|
||
hydrateSearchClient(client, initialResults); | ||
|
||
expect(client._cacheHydrated).not.toBeDefined(); | ||
}); | ||
|
||
it('should hydrate the client for >= v4 if the cache is enabled and the Algolia agent is present', () => { | ||
const setCache = jest.fn(); | ||
client = { | ||
transporter: { responsesCache: { set: setCache } }, | ||
addAlgoliaAgent: jest.fn(), | ||
} as unknown as SearchClient; | ||
|
||
hydrateSearchClient(client, initialResults); | ||
|
||
expect(setCache).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
args: [[{ indexName: 'instant_search', params: 'params' }]], | ||
method: 'search', | ||
}), | ||
expect.objectContaining({ | ||
results: [{ index: 'instant_search', params: 'params', nbHits: 1000 }], | ||
}) | ||
); | ||
expect(client._cacheHydrated).toBe(true); | ||
expect(client.search).toBeDefined(); | ||
}); | ||
|
||
it('should populate the cache for < v4 if there is no transporter object', () => { | ||
client = { | ||
addAlgoliaAgent: jest.fn(), | ||
_useCache: true, | ||
} as unknown as SearchClient; | ||
|
||
hydrateSearchClient(client, initialResults); | ||
|
||
expect(client.cache).toBeDefined(); | ||
}); | ||
}); |
132 changes: 132 additions & 0 deletions
132
packages/instantsearch.js/src/lib/utils/hydrateSearchClient.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// @ts-nocheck (types to be fixed during actual implementation) | ||
import type { InitialResults, SearchClient } from '../../types'; | ||
|
||
export function hydrateSearchClient( | ||
client: SearchClient, | ||
results?: InitialResults | ||
) { | ||
if (!results) { | ||
return; | ||
} | ||
|
||
// Disable cache hydration on: | ||
// - Algoliasearch API Client < v4 with cache disabled | ||
// - Third party clients (detected by the `addAlgoliaAgent` function missing) | ||
|
||
if ( | ||
(!client.transporter || client._cacheHydrated) && | ||
(!client._useCache || typeof client.addAlgoliaAgent !== 'function') | ||
) { | ||
return; | ||
} | ||
|
||
// Algoliasearch API Client >= v4 | ||
// To hydrate the client we need to populate the cache with the data from | ||
// the server (done in `hydrateSearchClientWithMultiIndexRequest` or | ||
// `hydrateSearchClientWithSingleIndexRequest`). But since there is no way | ||
// for us to compute the key the same way as `algoliasearch-client` we need | ||
// to populate it on a custom key and override the `search` method to | ||
// search on it first. | ||
if (client.transporter && !client._cacheHydrated) { | ||
client._cacheHydrated = true; | ||
|
||
const baseMethod = client.search; | ||
client.search = (requests, ...methodArgs) => { | ||
const requestsWithSerializedParams = requests.map((request) => ({ | ||
...request, | ||
params: serializeQueryParameters(request.params), | ||
})); | ||
|
||
return client.transporter.responsesCache.get( | ||
{ | ||
method: 'search', | ||
args: [requestsWithSerializedParams, ...methodArgs], | ||
}, | ||
() => { | ||
return baseMethod(requests, ...methodArgs); | ||
} | ||
); | ||
}; | ||
|
||
// Populate the cache with the data from the server | ||
client.transporter.responsesCache.set( | ||
{ | ||
method: 'search', | ||
args: [ | ||
Object.keys(results).reduce( | ||
(acc, key) => | ||
acc.concat( | ||
results[key].results.map((request) => ({ | ||
indexName: request.index, | ||
params: request.params, | ||
})) | ||
), | ||
[] | ||
), | ||
], | ||
}, | ||
{ | ||
results: Object.keys(results).reduce( | ||
(acc, key) => acc.concat(results[key].results), | ||
[] | ||
), | ||
} | ||
); | ||
} | ||
|
||
// Algoliasearch API Client < v4 | ||
// Prior to client v4 we didn't have a proper API to hydrate the client | ||
// cache from the outside. The following code populates the cache with | ||
// a single-index result. You can find more information about the | ||
// computation of the key inside the client (see link below). | ||
// https://github.com/algolia/algoliasearch-client-javascript/blob/c27e89ff92b2a854ae6f40dc524bffe0f0cbc169/src/AlgoliaSearchCore.js#L232-L240 | ||
if (!client.transporter) { | ||
const cacheKey = `/1/indexes/*/queries_body_${JSON.stringify({ | ||
requests: Object.keys(results).reduce( | ||
(acc, key) => | ||
acc.concat( | ||
results[key].rawResults.map((request) => ({ | ||
indexName: request.index, | ||
params: request.params, | ||
})) | ||
), | ||
[] | ||
), | ||
})}`; | ||
|
||
client.cache = { | ||
...client.cache, | ||
[cacheKey]: JSON.stringify({ | ||
results: Object.keys(results).reduce( | ||
(acc, key) => acc.concat(results[key].rawResults), | ||
[] | ||
), | ||
}), | ||
}; | ||
} | ||
} | ||
|
||
// This function is copied from the algoliasearch v4 API Client. If modified, | ||
// consider updating it also in `serializeQueryParameters` from `@algolia/transporter`. | ||
function serializeQueryParameters(parameters) { | ||
const isObjectOrArray = (value) => | ||
Object.prototype.toString.call(value) === '[object Object]' || | ||
Object.prototype.toString.call(value) === '[object Array]'; | ||
|
||
const encode = (format, ...args) => { | ||
let i = 0; | ||
return format.replace(/%s/g, () => encodeURIComponent(args[i++])); | ||
}; | ||
|
||
return Object.keys(parameters) | ||
.map((key) => | ||
encode( | ||
'%s=%s', | ||
key, | ||
isObjectOrArray(parameters[key]) | ||
? JSON.stringify(parameters[key]) | ||
: parameters[key] | ||
) | ||
) | ||
.join('&'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.