@@ -6,18 +6,15 @@ import {
66} from 'algoliasearch/lite'
77
88/**
9- * Algolia search client for npm packages.
10- * Credentials and index name come from runtimeConfig.public.algolia.
9+ * Singleton Algolia client, keyed by appId to handle config changes.
1110 */
1211let _searchClient : LiteClient | null = null
1312let _configuredAppId : string | null = null
1413
15- function getAlgoliaClient ( ) : LiteClient {
16- const { algolia } = useRuntimeConfig ( ) . public
17- // Re-create client if app ID changed (shouldn't happen, but be safe)
18- if ( ! _searchClient || _configuredAppId !== algolia . appId ) {
19- _searchClient = algoliasearch ( algolia . appId , algolia . apiKey )
20- _configuredAppId = algolia . appId
14+ function getOrCreateClient ( appId : string , apiKey : string ) : LiteClient {
15+ if ( ! _searchClient || _configuredAppId !== appId ) {
16+ _searchClient = algoliasearch ( appId , apiKey )
17+ _configuredAppId = appId
2118 }
2219 return _searchClient
2320}
@@ -125,92 +122,111 @@ export interface AlgoliaSearchOptions {
125122}
126123
127124/**
128- * Search npm packages via Algolia.
129- * Returns results in the same NpmSearchResponse format as the npm registry API.
130- */
131- export async function searchAlgolia (
132- query : string ,
133- options : AlgoliaSearchOptions = { } ,
134- ) : Promise < NpmSearchResponse > {
135- const client = getAlgoliaClient ( )
136-
137- const { results } = await client . search ( [
138- {
139- indexName : 'npm-search' ,
140- params : {
141- query,
142- offset : options . from ,
143- length : options . size ,
144- filters : options . filters || '' ,
145- analyticsTags : [ 'npmx.dev' ] ,
146- attributesToRetrieve : ATTRIBUTES_TO_RETRIEVE ,
147- attributesToHighlight : [ ] ,
148- } ,
149- } ,
150- ] )
151-
152- const response = results [ 0 ] as SearchResponse < AlgoliaHit >
153-
154- return {
155- isStale : false ,
156- objects : response . hits . map ( hitToSearchResult ) ,
157- total : response . nbHits ! ,
158- time : new Date ( ) . toISOString ( ) ,
159- }
160- }
161-
162- /**
163- * Fetch all packages in an Algolia scope (org or user).
164- * Uses facet filters for efficient server-side filtering.
125+ * Composable that provides Algolia search functions for npm packages.
165126 *
166- * For orgs: filters by `owner.name:orgname` which matches scoped packages.
167- * For users: filters by `owner.name:username` which matches maintainer.
127+ * Must be called during component setup (or inside another composable)
128+ * because it reads from `useRuntimeConfig()`. The returned functions
129+ * are safe to call at any time (event handlers, async callbacks, etc.).
168130 */
169- export async function searchAlgoliaByOwner (
170- ownerName : string ,
171- options : { maxResults ?: number } = { } ,
172- ) : Promise < NpmSearchResponse > {
173- const client = getAlgoliaClient ( )
174- const max = options . maxResults ?? 1000
175-
176- const allHits : AlgoliaHit [ ] = [ ]
177- let offset = 0
178- const batchSize = 200
179-
180- // Algolia supports up to 1000 results per query with offset/length pagination
181- while ( offset < max ) {
182- const length = Math . min ( batchSize , max - offset )
183-
131+ export function useAlgoliaSearch ( ) {
132+ const { algolia } = useRuntimeConfig ( ) . public
133+ const client = getOrCreateClient ( algolia . appId , algolia . apiKey )
134+ const indexName = algolia . indexName
135+
136+ /**
137+ * Search npm packages via Algolia.
138+ * Returns results in the same NpmSearchResponse format as the npm registry API.
139+ */
140+ async function search (
141+ query : string ,
142+ options : AlgoliaSearchOptions = { } ,
143+ ) : Promise < NpmSearchResponse > {
184144 const { results } = await client . search ( [
185145 {
186- indexName : 'npm-search' ,
146+ indexName,
187147 params : {
188- query : '' ,
189- offset,
190- length,
191- filters : `owner.name: ${ ownerName } ` ,
148+ query,
149+ offset : options . from ,
150+ length : options . size ,
151+ filters : options . filters || '' ,
192152 analyticsTags : [ 'npmx.dev' ] ,
193153 attributesToRetrieve : ATTRIBUTES_TO_RETRIEVE ,
194154 attributesToHighlight : [ ] ,
195155 } ,
196156 } ,
197157 ] )
198158
199- const response = results [ 0 ] as SearchResponse < AlgoliaHit >
200- allHits . push ( ...response . hits )
159+ const response = results [ 0 ] as SearchResponse < AlgoliaHit > | undefined
160+ if ( ! response ) {
161+ return { isStale : false , objects : [ ] , total : 0 , time : new Date ( ) . toISOString ( ) }
162+ }
163+
164+ return {
165+ isStale : false ,
166+ objects : response . hits . map ( hitToSearchResult ) ,
167+ total : response . nbHits ?? 0 ,
168+ time : new Date ( ) . toISOString ( ) ,
169+ }
170+ }
171+
172+ /**
173+ * Fetch all packages for an Algolia owner (org or user).
174+ * Uses `owner.name` filter for efficient server-side filtering.
175+ */
176+ async function searchByOwner (
177+ ownerName : string ,
178+ options : { maxResults ?: number } = { } ,
179+ ) : Promise < NpmSearchResponse > {
180+ const max = options . maxResults ?? 1000
181+
182+ const allHits : AlgoliaHit [ ] = [ ]
183+ let offset = 0
184+ const batchSize = 200
185+
186+ // Algolia supports up to 1000 results per query with offset/length pagination
187+ while ( offset < max ) {
188+ const length = Math . min ( batchSize , max - offset )
189+
190+ const { results } = await client . search ( [
191+ {
192+ indexName,
193+ params : {
194+ query : '' ,
195+ offset,
196+ length,
197+ filters : `owner.name:${ ownerName } ` ,
198+ analyticsTags : [ 'npmx.dev' ] ,
199+ attributesToRetrieve : ATTRIBUTES_TO_RETRIEVE ,
200+ attributesToHighlight : [ ] ,
201+ } ,
202+ } ,
203+ ] )
204+
205+ const response = results [ 0 ] as SearchResponse < AlgoliaHit > | undefined
206+ if ( ! response ) break
201207
202- // If we got fewer than requested, we've exhausted all results
203- if ( response . hits . length < length || allHits . length >= response . nbHits ! ) {
204- break
208+ allHits . push ( ...response . hits )
209+
210+ // If we got fewer than requested, we've exhausted all results
211+ if ( response . hits . length < length || allHits . length >= ( response . nbHits ?? 0 ) ) {
212+ break
213+ }
214+
215+ offset += length
205216 }
206217
207- offset += length
218+ return {
219+ isStale : false ,
220+ objects : allHits . map ( hitToSearchResult ) ,
221+ total : allHits . length ,
222+ time : new Date ( ) . toISOString ( ) ,
223+ }
208224 }
209225
210226 return {
211- isStale : false ,
212- objects : allHits . map ( hitToSearchResult ) ,
213- total : allHits . length ,
214- time : new Date ( ) . toISOString ( ) ,
227+ /** Search packages by text query */
228+ search ,
229+ /** Fetch all packages for an owner (org or user) */
230+ searchByOwner ,
215231 }
216232}
0 commit comments