Skip to content

Commit

Permalink
fix(createInstantSearchManager): drop outdated response (#1765)
Browse files Browse the repository at this point in the history
The helper is implemented in a way that it will drop outdated requests.
This change makes react-instantsearch use the event model of the helper
that let it implement this mechanism that ensures reliability in the
algolia answers. It also results in some fewer instanciation of objects
for callbacks and the base search parameters.
  • Loading branch information
bobylito committed Dec 22, 2016
1 parent 715ea85 commit 76c5312
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 236 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-env jest, jasmine */
/* eslint-disable no-console */

import createInstantSearchManager from './createInstantSearchManager';

import algoliaClient from 'algoliasearch';

jest.useFakeTimers();

jest.mock('algoliasearch-helper/src/algoliasearch.helper.js', () => {
let count = 0;
console.log('setup');
const Helper = require.requireActual('algoliasearch-helper/src/algoliasearch.helper.js');
Helper.prototype._handleResponse = function(state) {
this.emit('error', {count: count++}, state);
};
return Helper;
});

const client = algoliaClient('latency', '249078a3d4337a8231f1665ec5a44966');
client.search = jest.fn((queries, cb) => {
if (cb) {
setImmediate(() => {
// We do not care about the returned values because we also control how
// it will handle in the helper
cb(null, null);
});
return undefined;
}

return new Promise(resolve => {
// cf comment above
resolve(null);
});
});

describe('createInstantSearchManager', () => {
describe('with error from algolia', () => {
describe('on widget lifecycle', () => {
it('updates the store and searches', () => {
const ism = createInstantSearchManager({
indexName: 'index',
initialState: {},
searchParameters: {},
algoliaClient: client,
});

ism.widgetsManager.registerWidget({
getSearchParameters: params => params.setQuery('search'),
});

expect(ism.store.getState().error).toBe(null);

jest.runAllTimers();

const store = ism.store.getState();
expect(store.error).toEqual({count: 0});
expect(store.results).toBe(null);

ism.widgetsManager.update();

jest.runAllTimers();

const store1 = ism.store.getState();
expect(store1.error).toEqual({count: 1});
expect(store1.results).toBe(null);
});
});
describe('on external updates', () => {
it('updates the store and searches', () => {
const ism = createInstantSearchManager({
indexName: 'index',
initialState: {},
searchParameters: {},
algoliaClient: client,
});

ism.onExternalStateUpdate({});

expect(ism.store.getState().error).toBe(null);

jest.runAllTimers();

const store = ism.store.getState();
expect(store.error).toEqual({count: 2});
expect(store.results).toBe(null);
});
});
});
});
65 changes: 32 additions & 33 deletions packages/react-instantsearch/src/core/createInstantSearchManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ import highlightTags from './highlightTags.js';
*/
export default function createInstantSearchManager({
indexName,
initialState,
initialState = {},
algoliaClient,
searchParameters = {},
}) {
const helper = algoliasearchHelper(algoliaClient);
const baseSP = new SearchParameters({
...searchParameters,
index: indexName,
...highlightTags,
});

const helper = algoliasearchHelper(algoliaClient, indexName, baseSP);
helper.on('result', handleSearchSuccess);
helper.on('error', handleSearchError);

const initialSearchParameters = helper.state;

const widgetsManager = createWidgetsManager(onWidgetsUpdate);

Expand All @@ -36,7 +46,7 @@ export default function createInstantSearchManager({
.map(widget => widget.getMetadata(state));
}

function getSearchParameters(initialSearchParameters) {
function getSearchParameters() {
return widgetsManager.getWidgets()
.filter(widget => Boolean(widget.getSearchParameters))
.reduce(
Expand All @@ -46,37 +56,26 @@ export default function createInstantSearchManager({
}

function search() {
const baseSP = new SearchParameters({
...searchParameters,
index: indexName,
...highlightTags,
const widgetSearchParameters = getSearchParameters(helper.state);

helper.setState(widgetSearchParameters)
.search();
}

function handleSearchSuccess(content) {
store.setState({
...store.getState(),
results: content,
searching: false,
});
}

function handleSearchError(error) {
store.setState({
...store.getState(),
error,
searching: false,
});
const widgetSearchParameters = getSearchParameters(baseSP);

helper
.searchOnce(widgetSearchParameters)
.then(({content}) => {
store.setState({
...store.getState(),
results: content,
searching: false,
});
}, error => {
store.setState({
...store.getState(),
error,
searching: false,
});
})
.catch(error => {
// Since setState is synchronous, any error that occurs in the render of a
// component will be swallowed by this promise.
// This is a trick to make the error show up correctly in the console.
// See http://stackoverflow.com/a/30741722/969302
setTimeout(() => {
throw error;
});
});
}

// Called whenever a widget has been rendered with new props.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-env jest, jasmine */
/* eslint-disable no-console */

import createInstantSearchManager from './createInstantSearchManager';

import algoliaClient from 'algoliasearch';

jest.useFakeTimers();

jest.mock('algoliasearch-helper/src/algoliasearch.helper.js', () => {
let count = 0;
const Helper = require.requireActual('algoliasearch-helper/src/algoliasearch.helper.js');
Helper.prototype._handleResponse = function(state) {
this.emit('result', {count: count++}, state);
};
return Helper;
});

const client = algoliaClient('latency', '249078a3d4337a8231f1665ec5a44966');
client.search = jest.fn((queries, cb) => {
if (cb) {
setImmediate(() => {
// We do not care about the returned values because we also control how
// it will handle in the helper
cb(null, null);
});
return undefined;
}

return new Promise(resolve => {
// cf comment above
resolve(null);
});
});

describe('createInstantSearchManager', () => {
describe('with correct result from algolia', () => {
describe('on widget lifecycle', () => {
it('updates the store and searches', () => {
const ism = createInstantSearchManager({
indexName: 'index',
initialState: {},
searchParameters: {},
algoliaClient: client,
});

ism.widgetsManager.registerWidget({
getSearchParameters: params => params.setQuery('search'),
});

expect(ism.store.getState().results).toBe(null);

jest.runAllTimers();

const store = ism.store.getState();
expect(store.results).toEqual({count: 0});
expect(store.error).toBe(null);

ism.widgetsManager.update();

jest.runAllTimers();

const store1 = ism.store.getState();
expect(store1.results).toEqual({count: 1});
expect(store1.error).toBe(null);
});
});
describe('on external updates', () => {
it('updates the store and searches', () => {
const ism = createInstantSearchManager({
indexName: 'index',
initialState: {},
searchParameters: {},
algoliaClient: client,
});

ism.onExternalStateUpdate({});

expect(ism.store.getState().results).toBe(null);

jest.runAllTimers();

const store = ism.store.getState();
expect(store.results).toEqual({count: 2});
expect(store.error).toBe(null);
});
});
});
});

0 comments on commit 76c5312

Please sign in to comment.