-
Notifications
You must be signed in to change notification settings - Fork 498
/
connectAnswers.ts
264 lines (228 loc) · 6.52 KB
/
connectAnswers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import {
checkRendering,
createDocumentationMessageGenerator,
createConcurrentSafePromise,
addQueryID,
debounce,
addAbsolutePosition,
noop,
escapeHits,
} from '../../lib/utils';
import type { DebouncedFunction } from '../../lib/utils/debounce';
import type {
Connector,
Hits,
Hit,
FindAnswersOptions,
FindAnswersParameters,
FindAnswersResponse,
WidgetRenderState,
} from '../../types';
type IndexWithAnswers = {
readonly findAnswers: any;
};
function hasFindAnswersMethod(
answersIndex: IndexWithAnswers | any
): answersIndex is IndexWithAnswers {
return typeof (answersIndex as IndexWithAnswers).findAnswers === 'function';
}
const withUsage = createDocumentationMessageGenerator({
name: 'answers',
connector: true,
});
export type AnswersRenderState = {
/**
* The matched hits from Algolia API.
*/
hits: Hits;
/**
* Whether it's still loading the results from the Answers API.
*/
isLoading: boolean;
};
export type AnswersConnectorParams = {
/**
* Attributes to use for predictions.
* If empty, we use all `searchableAttributes` to find answers.
* All your `attributesForPrediction` must be part of your `searchableAttributes`.
*/
attributesForPrediction?: string[];
/**
* The languages in the query. Currently only supports `en`.
*/
queryLanguages: ['en'];
/**
* Maximum number of answers to retrieve from the Answers Engine.
* Cannot be greater than 1000.
* @default 1
*/
nbHits?: number;
/**
* Debounce time in milliseconds to debounce render
* @default 100
*/
renderDebounceTime?: number;
/**
* Debounce time in milliseconds to debounce search
* @default 100
*/
searchDebounceTime?: number;
/**
* Whether to escape HTML tags from hits string values.
*
* @default true
*/
escapeHTML?: boolean;
/**
* Extra parameters to pass to findAnswers method.
* @default {}
*/
extraParameters?: FindAnswersOptions;
};
export type AnswersWidgetDescription = {
$$type: 'ais.answers';
renderState: AnswersRenderState;
indexRenderState: {
answers: WidgetRenderState<AnswersRenderState, AnswersConnectorParams>;
};
};
export type AnswersConnector = Connector<
AnswersWidgetDescription,
AnswersConnectorParams
>;
const connectAnswers: AnswersConnector = function connectAnswers(
renderFn,
unmountFn = noop
) {
checkRendering(renderFn, withUsage());
return (widgetParams) => {
const {
queryLanguages,
attributesForPrediction,
nbHits = 1,
renderDebounceTime = 100,
searchDebounceTime = 100,
escapeHTML = true,
extraParameters = {},
} = widgetParams || {};
// @ts-expect-error checking for the wrong value
if (!queryLanguages || queryLanguages.length === 0) {
throw new Error(
withUsage('The `queryLanguages` expects an array of strings.')
);
}
const runConcurrentSafePromise =
createConcurrentSafePromise<FindAnswersResponse<Hit>>();
let lastResult: Partial<FindAnswersResponse<Hit>>;
let isLoading = false;
const debouncedRender = debounce(renderFn, renderDebounceTime);
// this does not directly use DebouncedFunction<findAnswers>, since then the generic will disappear
let debouncedRefine: DebouncedFunction<
(...params: FindAnswersParameters) => Promise<FindAnswersResponse<Hit>>
>;
return {
$$type: 'ais.answers',
init(initOptions) {
const { state, instantSearchInstance } = initOptions;
const answersIndex = instantSearchInstance.client.initIndex!(
state.index
);
if (!hasFindAnswersMethod(answersIndex)) {
throw new Error(withUsage('`algoliasearch` >= 4.8.0 required.'));
}
debouncedRefine = debounce(
answersIndex.findAnswers,
searchDebounceTime
);
renderFn(
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance: initOptions.instantSearchInstance,
},
true
);
},
render(renderOptions) {
const query = renderOptions.state.query;
if (!query) {
// renders nothing with empty query
lastResult = {};
isLoading = false;
renderFn(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance: renderOptions.instantSearchInstance,
},
false
);
return;
}
// render the loader
lastResult = {};
isLoading = true;
renderFn(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance: renderOptions.instantSearchInstance,
},
false
);
// call /answers API
runConcurrentSafePromise(
debouncedRefine(query, queryLanguages, {
...extraParameters,
nbHits,
attributesForPrediction,
})
).then((results) => {
if (!results) {
// It's undefined when it's debounced.
return;
}
if (escapeHTML && results.hits.length > 0) {
results.hits = escapeHits(results.hits);
}
const initialEscaped = (results.hits as ReturnType<typeof escapeHits>)
.__escaped;
results.hits = addAbsolutePosition(results.hits, 0, nbHits);
results.hits = addQueryID(results.hits, results.queryID);
// Make sure the escaped tag stays, even after mapping over the hits.
// This prevents the hits from being double-escaped if there are multiple
// hits widgets mounted on the page.
(results.hits as ReturnType<typeof escapeHits>).__escaped =
initialEscaped;
lastResult = results;
isLoading = false;
debouncedRender(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance: renderOptions.instantSearchInstance,
},
false
);
});
},
getRenderState(renderState, renderOptions) {
return {
...renderState,
answers: this.getWidgetRenderState(renderOptions),
};
},
getWidgetRenderState() {
return {
hits: lastResult?.hits || [],
isLoading,
widgetParams,
};
},
dispose({ state }) {
unmountFn();
return state;
},
getWidgetSearchParameters(state) {
return state;
},
};
};
};
export default connectAnswers;