import type { UiState, Connector, WidgetDescription, Widget } from "instantsearch.js"; import type { IndexWidget } from "instantsearch.js/es/widgets/index/index"; import { readable, writable, type Readable } from "svelte/store"; import algoliasearchHelper from "algoliasearch-helper"; import type { SearchParameters } from "algoliasearch-helper"; import { getInstantSearchContext } from "./instantSearchContext"; import { getIndexContext } from "./indexContext"; import { onDestroyClientSide } from "./utils"; /* createSearchResults and getIndexSearchResults were gotten from https://github.com/algolia/react-instantsearch/blob/v6.31.1/packages/react-instantsearch-hooks/src/lib/getIndexSearchResults.ts and https://github.com/algolia/react-instantsearch/blob/v6.31.1/packages/react-instantsearch-hooks/src/lib/createSearchResults.ts */ function createSearchResults(state: SearchParameters) { return new algoliasearchHelper.SearchResults( state, [ { query: state.query ?? "", page: state.page ?? 0, hitsPerPage: state.hitsPerPage ?? 20, hits: [], nbHits: 0, nbPages: 0, params: "", exhaustiveNbHits: true, exhaustiveFacetsCount: true, processingTimeMS: 0, index: state.index, }, ], { /** used by connectors to prevent persisting these results */ __isArtificial: true, }, ); } function getIndexSearchResults(indexWidget: IndexWidget) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const helper = indexWidget.getHelper()!; const results = // On SSR, we get the results injected on the Index. indexWidget.getResults() || // On the browser, we create fallback results based on the helper state. createSearchResults(helper.state); const scopedResults = indexWidget.getScopedResults().map((scopedResult) => { const fallbackResults = scopedResult.indexId === indexWidget.getIndexId() ? results : createSearchResults(scopedResult.helper.state); return { ...scopedResult, // We keep `results` from being `null`. results: scopedResult.results || fallbackResults, }; }); return { results, scopedResults, }; } // With some tricky typing voodoo we're able to get proper state type inference when using connect // eslint-disable-next-line @typescript-eslint/no-explicit-any type ExtractStateType any> = Parameters[0]>[0]; type AdditionalWidgetProperties = Partial>; export default function connect>>( connector: T, widgetParams: Parameters>[0] = {}, additionalWidgetProperties: AdditionalWidgetProperties = {}, ): Readable> { const search = getInstantSearchContext(); const parentIndex = getIndexContext(); // Originally just used readable which means I had to initialize widget // in the readable start function, however this keeps us from setting initial state. // Could be nice to have something like widget.subscribe() :) const writableState = writable>(); const widget = { ...connector(writableState.set)(widgetParams), ...additionalWidgetProperties }; parentIndex.addWidgets([widget]); onDestroyClientSide(() => { parentIndex.removeWidgets([widget]); }); // We want to get the initial state so that it will not be null at first render. let initialState: ExtractStateType = {} as ExtractStateType; if (widget.getWidgetRenderState) { // The helper exists because we've started InstantSearch. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const helper = parentIndex.getHelper()!; const uiState = parentIndex.getWidgetUiState({})[parentIndex.getIndexId()]; helper.state = widget.getWidgetSearchParameters?.(helper.state, { uiState }) || helper.state; const { results, scopedResults } = getIndexSearchResults(parentIndex); // We get the widget render state by providing the same parameters as // InstantSearch provides to the widget's `render` method. // See https://github.com/algolia/instantsearch.js/blob/019cd18d0de6dd320284aa4890541b7fe2198c65/src/widgets/index/index.ts#L604-L617 // eslint-disable-next-line @typescript-eslint/no-unused-vars const { widgetParams, ...renderState } = widget.getWidgetRenderState({ helper, parent: parentIndex, instantSearchInstance: search, results, scopedResults, state: helper.state, renderState: search.renderState, templatesConfig: search.templatesConfig, createURL: parentIndex.createURL, searchMetadata: { isSearchStalled: search.status === "stalled", }, error: search.error, status: search.status, }); initialState = renderState as ExtractStateType; } // We set the writable state to the initial state so that it does not overwrite readable with null. writableState.set(initialState); // We return a readable because it makes more sense than a writable :) return readable>(initialState, (set) => writableState.subscribe(set)); }