11import { getCodeHostBrowseFileAtBranchUrl } from "@/lib/utils" ;
2+ import { unexpectedError } from "@/lib/serviceError" ;
23import type { ProtoGrpcType } from '@/proto/webserver' ;
34import { FileMatch__Output as ZoektGrpcFileMatch } from "@/proto/zoekt/webserver/v1/FileMatch" ;
45import { FlushReason as ZoektGrpcFlushReason } from "@/proto/zoekt/webserver/v1/FlushReason" ;
@@ -15,7 +16,7 @@ import { PrismaClient, Repo } from "@sourcebot/db";
1516import { createLogger , env } from "@sourcebot/shared" ;
1617import path from 'path' ;
1718import { QueryIR , someInQueryIR } from './ir' ;
18- import { RepositoryInfo , SearchResponse , SearchResultFile , SearchStats , SourceRange , StreamedSearchResponse } from "./types" ;
19+ import { RepositoryInfo , SearchResponse , SearchResultFile , SearchStats , SourceRange , StreamedSearchErrorResponse , StreamedSearchResponse } from "./types" ;
1920
2021const logger = createLogger ( "zoekt-searcher" ) ;
2122
@@ -177,8 +178,8 @@ export const zoektStreamSearch = async (searchRequest: ZoektGrpcSearchRequest, p
177178 isSearchExhaustive : accumulatedStats . totalMatchCount <= accumulatedStats . actualMatchCount ,
178179 }
179180
180- controller . enqueue ( new TextEncoder ( ) . encode ( `data: ${ JSON . stringify ( finalResponse ) } \n\n` ) ) ;
181- controller . enqueue ( new TextEncoder ( ) . encode ( 'data: [DONE]\n\n ') ) ;
181+ controller . enqueue ( encodeSSEREsponseChunk ( finalResponse ) ) ;
182+ controller . enqueue ( encodeSSEREsponseChunk ( ' [DONE]') ) ;
182183 controller . close ( ) ;
183184 client . close ( ) ;
184185 logger . debug ( 'SSE stream closed' ) ;
@@ -231,10 +232,18 @@ export const zoektStreamSearch = async (searchRequest: ZoektGrpcSearchRequest, p
231232 stats
232233 }
233234
234- const sseData = `data: ${ JSON . stringify ( response ) } \n\n` ;
235- controller . enqueue ( new TextEncoder ( ) . encode ( sseData ) ) ;
235+ controller . enqueue ( encodeSSEREsponseChunk ( response ) ) ;
236236 } catch ( error ) {
237- console . error ( 'Error encoding chunk:' , error ) ;
237+ logger . error ( 'Error processing chunk:' , error ) ;
238+ Sentry . captureException ( error ) ;
239+ isStreamActive = false ;
240+
241+ const errorMessage = error instanceof Error ? error . message : 'Unknown error processing chunk' ;
242+ const errorResponse : StreamedSearchErrorResponse = {
243+ type : 'error' ,
244+ error : unexpectedError ( errorMessage ) ,
245+ } ;
246+ controller . enqueue ( encodeSSEREsponseChunk ( errorResponse ) ) ;
238247 } finally {
239248 pendingChunks -- ;
240249 grpcStream ?. resume ( ) ;
@@ -270,26 +279,26 @@ export const zoektStreamSearch = async (searchRequest: ZoektGrpcSearchRequest, p
270279 }
271280 isStreamActive = false ;
272281
273- // Send error as SSE event
274- const errorData = `data: ${ JSON . stringify ( {
275- error : {
276- code : error . code ,
277- message : error . details || error . message ,
278- }
279- } ) } \n\n`;
280- controller . enqueue ( new TextEncoder ( ) . encode ( errorData ) ) ;
282+ // Send properly typed error response
283+ const errorResponse : StreamedSearchErrorResponse = {
284+ type : 'error' ,
285+ error : unexpectedError ( error . details || error . message ) ,
286+ } ;
287+ controller . enqueue ( encodeSSEREsponseChunk ( errorResponse ) ) ;
281288
282289 controller . close ( ) ;
283290 client . close ( ) ;
284291 } ) ;
285292 } catch ( error ) {
286293 logger . error ( 'Stream initialization error:' , error ) ;
294+ Sentry . captureException ( error ) ;
287295
288296 const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
289- const errorData = `data: ${ JSON . stringify ( {
290- error : { message : errorMessage }
291- } ) } \n\n`;
292- controller . enqueue ( new TextEncoder ( ) . encode ( errorData ) ) ;
297+ const errorResponse : StreamedSearchErrorResponse = {
298+ type : 'error' ,
299+ error : unexpectedError ( errorMessage ) ,
300+ } ;
301+ controller . enqueue ( encodeSSEREsponseChunk ( errorResponse ) ) ;
293302
294303 controller . close ( ) ;
295304 client . close ( ) ;
@@ -309,6 +318,12 @@ export const zoektStreamSearch = async (searchRequest: ZoektGrpcSearchRequest, p
309318 } ) ;
310319}
311320
321+ // Encodes a response chunk into a SSE-compatible format.
322+ const encodeSSEREsponseChunk = ( response : object | string ) => {
323+ const data = typeof response === 'string' ? response : JSON . stringify ( response ) ;
324+ return new TextEncoder ( ) . encode ( `data: ${ data } \n\n` ) ;
325+ }
326+
312327// Creates a mapping between all repository ids in a given response
313328// chunk. The mapping allows us to efficiently lookup repository metadata.
314329const createReposMapForChunk = async ( chunk : ZoektGrpcSearchResponse , reposMapCache : Map < string | number , Repo > , prisma : PrismaClient ) : Promise < Map < string | number , Repo > > => {
0 commit comments