-
Notifications
You must be signed in to change notification settings - Fork 529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(insights): introduce insights
middleware (1/4)
#4446
Changes from 22 commits
3963956
4331d24
77996c9
467a648
ec3f7bb
3ab96a2
020381e
a0cc96f
c642234
03e52c7
0ae09e2
2417c50
090fe28
c262700
2a49909
20254cf
10ce292
5a1fc34
963d305
daca533
000d708
602037a
8fd4698
b0b7add
e2a2743
21faadb
7f49f25
94e394b
ad0dafb
f34170d
ef95a87
183b1ca
e7c7e58
60af9e0
5067ec7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1 @@ | ||
import { UiState } from '../types'; | ||
|
||
export type MiddlewareDefinition = { | ||
onStateChange(options: { uiState: UiState }): void; | ||
subscribe(): void; | ||
unsubscribe(): void; | ||
}; | ||
|
||
export type Middleware = ({ | ||
instantSearchInstance: InstantSearch, | ||
}) => MiddlewareDefinition; | ||
|
||
export { createRouter, RouterProps } from './createRouter'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if anyone uses this, but this changes the API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
export { createInsightsMiddleware } from './insights'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { InsightsClient, Middleware } from '../types'; | ||
import { getInsightsAnonymousUserToken } from '../helpers'; | ||
import { warning, noop } from '../lib/utils'; | ||
|
||
export type InsightsProps = { | ||
insightsClient: false | InsightsClient; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accepting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null is a fair option, I suggested EDIT: it's actually There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, |
||
}; | ||
|
||
export type CreateInsightsMiddleware = (props: InsightsProps) => Middleware; | ||
|
||
export const createInsightsMiddleware: CreateInsightsMiddleware = props => { | ||
const { insightsClient: _insightsClient } = props; | ||
if (_insightsClient !== false && !_insightsClient) { | ||
if (__DEV__) { | ||
throw new Error( | ||
"The `insightsClient` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to `false`." | ||
); | ||
} else { | ||
throw new Error( | ||
'The `insightsClient` option is required. To disable, set it to `false`.' | ||
); | ||
} | ||
} | ||
|
||
const hasInsightsClient = Boolean(_insightsClient); | ||
const insightsClient = | ||
_insightsClient === false ? (noop as InsightsClient) : _insightsClient; | ||
|
||
eunjae-lee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ({ instantSearchInstance }) => { | ||
insightsClient('_get', '_hasCredentials', (hasCredentials: boolean) => { | ||
if (!hasCredentials) { | ||
const [appId, apiKey] = getAppIdAndApiKey(instantSearchInstance.client); | ||
insightsClient('_get', '_userToken', (userToken: string) => { | ||
warning( | ||
!userToken, | ||
`You set userToken before \`createInsightsMiddleware()\` and it is ignored. | ||
Please set the token after the \`createInsightsMiddleware()\` call. | ||
|
||
createInsightsMiddleware({ /* ... */ }); | ||
|
||
insightsClient('setUserToken', 'your-user-token'); | ||
// or | ||
aa('setUserToken', 'your-user-token'); | ||
` | ||
); | ||
}); | ||
insightsClient('init', { appId, apiKey }); | ||
} | ||
}); | ||
|
||
return { | ||
onStateChange() {}, | ||
subscribe() { | ||
const setUserTokenToSearch = (userToken?: string) => { | ||
// At the time this middleware is subscribed, `mainIndex.init()` is already called. | ||
// It means `mainIndex.getHelper()` exists. | ||
if (userToken) { | ||
instantSearchInstance.mainIndex | ||
.getHelper()! | ||
.setQueryParameter('userToken', userToken); | ||
} | ||
}; | ||
|
||
instantSearchInstance.mainIndex | ||
.getHelper()! | ||
.setQueryParameter('clickAnalytics', true); | ||
|
||
if (hasInsightsClient) { | ||
// When `aa('init', { ... })` is called, it creates an anonymous user token in cookie. | ||
// We can set it as userToken. | ||
setUserTokenToSearch(getInsightsAnonymousUserToken()); | ||
} | ||
|
||
if (Array.isArray((insightsClient as any).queue)) { | ||
// Context: The umd build of search-insights is asynchronously loaded by the snippet. | ||
// | ||
// When user called `aa('setUserToken', 'my-user-token')` before `search-insights` is loaded, | ||
eunjae-lee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// it is stored in `aa.queue` and we are reading it to set userToken to search call. | ||
// This queue is meant to be consumed whenever `search-insights` is loaded and when it runs `processQueue()`. | ||
// But the reason why we handle it here is to prevent the first search API from being triggered | ||
// without userToken because search-insights is not loaded yet. | ||
(insightsClient as any).queue.forEach(([method, firstArgument]) => { | ||
if (method === 'setUserToken') { | ||
setUserTokenToSearch(firstArgument); | ||
} | ||
}); | ||
} | ||
|
||
// This updates userToken which is set explicitly by `aa('setUserToken', userToken)` | ||
insightsClient('onUserTokenChange', setUserTokenToSearch, { | ||
immediate: true, | ||
}); | ||
}, | ||
unsubscribe() { | ||
insightsClient('onUserTokenChange', undefined); | ||
}, | ||
}; | ||
}; | ||
}; | ||
|
||
function getAppIdAndApiKey(searchClient) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe add a unit test here. Since we run the tests with algoliasearch v3 and v4 separately, you should be able to cover both branches. Other case we can add a dev dependency for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL: "npm:algoliasearch@3" syntax. Nice! 7f49f25 |
||
if (searchClient.transporter) { | ||
// searchClient v4 | ||
const { headers, queryParameters } = searchClient.transporter; | ||
const APP_ID = 'x-algolia-application-id'; | ||
const API_KEY = 'x-algolia-api-key'; | ||
const appId = headers[APP_ID] || queryParameters[APP_ID]; | ||
const apiKey = headers[API_KEY] || queryParameters[API_KEY]; | ||
return [appId, apiKey]; | ||
} else { | ||
// searchClient v3 | ||
return [searchClient.applicationID, searchClient.apiKey]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import InstantSearch from '../lib/InstantSearch'; | ||
import { UiState } from './widget'; | ||
|
||
export type MiddlewareDefinition = { | ||
onStateChange(options: { uiState: UiState }): void; | ||
subscribe(): void; | ||
unsubscribe(): void; | ||
}; | ||
|
||
export type MiddlewareOptions = { | ||
instantSearchInstance: InstantSearch; | ||
}; | ||
|
||
export type Middleware = (options: MiddlewareOptions) => MiddlewareDefinition; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
middleware vs middlewares
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"middlewares" is not proper English.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://en.wiktionary.org/wiki/middleware
I don't think I've seen
s
at the end ofmiddleware
before, though.So you agree it's natural to have
middleware
although we haverouters, stateMappings, connectors, widgets
, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be middlewares, at least for the object containing all possible middlewares. Interestingly my spell-checker only accepts singular "middleware" as correct, and not "middlewares"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://www.wordhippo.com/what-is/the-plural-of/middleware.html
While
middleware
is more common, it's also safe to usemiddlewares
if we want to explicitly indicate it's a collection.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f34170d