Skip to content

Commit c5ac25e

Browse files
authored
nes: xtab: refactor: extract _performFetch from _streamEditsImpl (#308778)
refactor(xtab): extract _performFetch from _streamEditsImpl Separate the HTTP fetch lifecycle (FetchStreamSource setup, makeChatRequest2, initial error handling, line stream construction with cursor-tag removal and latency logging) into a dedicated _performFetch method. This makes the fetch infrastructure self-contained and returns a clean AsyncIterable<string> line stream + getFetchFailure callback for downstream format handlers, matching the pattern already used by XtabCustomDiffPatchResponseHandler. _streamEditsImpl is now a coordinator: it calls _performFetch, then dispatches to format handlers, then post-processes edit-window results. Part of #308744
1 parent 35a23cd commit c5ac25e

File tree

1 file changed

+81
-22
lines changed

1 file changed

+81
-22
lines changed

extensions/copilot/src/extension/xtab/node/xtabProvider.ts

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ interface FetchMetadata {
129129
userHappinessScore: number | undefined;
130130
}
131131

132+
namespace FetchResult {
133+
export class Lines {
134+
constructor(
135+
readonly linesStream: AsyncIterable<string>,
136+
readonly getFetchFailure: () => NoNextEditReason | undefined,
137+
readonly getResponseSoFar: () => string,
138+
readonly fetchRequestStopWatch: StopWatch,
139+
) { }
140+
}
141+
export class ModelNotFound { public static INSTANCE = new ModelNotFound(); }
142+
export class Error {
143+
constructor(readonly reason: NoNextEditReason) { }
144+
}
145+
146+
export type t = Lines | ModelNotFound | Error;
147+
}
148+
132149
export class XtabProvider implements IStatelessNextEditProvider {
133150

134151
public static readonly ID = XTabProviderId;
@@ -700,23 +717,27 @@ export class XtabProvider implements IStatelessNextEditProvider {
700717
}
701718
}
702719

703-
private async *_streamEditsImpl(
704-
request: StatelessNextEditRequest,
705-
editStreamCtx: EditStreamContext,
706-
responseOpts: ResponseOpts,
720+
/**
721+
* Initiates the HTTP fetch, sets up the streaming pipeline, and returns either
722+
* a clean line stream (with cursor-tag removal and latency logging applied)
723+
* or an error / retry signal.
724+
*
725+
* This method encapsulates all fetch infrastructure so that downstream response
726+
* format handlers only need an `AsyncIterable<string>` line stream.
727+
*/
728+
private async _performFetch(
729+
endpoint: IChatEndpoint,
730+
messages: Raw.ChatMessage[],
731+
prediction: Prediction | undefined,
732+
requestId: string,
707733
fetchMetadata: FetchMetadata,
708-
retryState: RetryState.t,
709-
delaySession: DelaySession,
710-
tracing: RequestTracingContext,
711-
cancellationToken: CancellationToken,
712-
fetchCts: CancellationTokenSource,
734+
shouldRemoveCursorTagFromResponse: boolean,
735+
editWindow: OffsetRange,
736+
documentBeforeEdits: StringText,
713737
fetchCancellationToken: CancellationToken,
714-
): EditStreaming {
738+
tracing: RequestTracingContext,
739+
): Promise<FetchResult.t> {
715740
const { tracer, logContext, telemetry } = tracing;
716-
const { endpoint, messages, clippedTaggedCurrentDoc, editWindowInfo, promptPieces, prediction, originalEditWindow } = editStreamCtx;
717-
const { editWindow, editWindowLines, cursorOriginalLinesOffset, editWindowLineRange } = editWindowInfo;
718-
719-
const targetDocument = request.getActiveDocument().id;
720741

721742
const useFetcher = this.configService.getExperimentBasedConfig(ConfigKey.NextEditSuggestionsFetcher, this.expService) || undefined;
722743

@@ -732,7 +753,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
732753

733754
const firstTokenReceived = new DeferredPromise<void>();
734755

735-
logContext.setHeaderRequestId(request.headerRequestId);
756+
logContext.setHeaderRequestId(requestId);
736757

737758
telemetry.setFetchStartedAt();
738759
logContext.setFetchStartTime();
@@ -765,7 +786,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
765786
} satisfies OptionalChatRequestParams,
766787
userInitiatedRequest: undefined,
767788
telemetryProperties: {
768-
requestId: request.headerRequestId,
789+
requestId,
769790
},
770791
useFetcher,
771792
customMetadata: {
@@ -785,13 +806,13 @@ export class XtabProvider implements IStatelessNextEditProvider {
785806
!this.forceUseDefaultModel // if we haven't already forced using the default model; otherwise, this could cause an infinite loop
786807
) {
787808
this.forceUseDefaultModel = true;
788-
return yield* this.doGetNextEdit(request, delaySession, tracing, cancellationToken, retryState); // use the same retry state
809+
return FetchResult.ModelNotFound.INSTANCE;
789810
}
790811
// diff-patch based model returns no choices if it has no edits to suggest
791812
if (fetchRes.type === ChatFetchResponseType.Unknown && fetchRes.reason === RESPONSE_CONTAINED_NO_CHOICES) {
792-
return new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, editWindow);
813+
return new FetchResult.Error(new NoNextEditReason.NoSuggestions(documentBeforeEdits, editWindow));
793814
}
794-
return mapChatFetcherErrorToNoNextEditReason(fetchRes);
815+
return new FetchResult.Error(mapChatFetcherErrorToNoNextEditReason(fetchRes));
795816
}
796817

797818
fetchResultPromise
@@ -829,15 +850,53 @@ export class XtabProvider implements IStatelessNextEditProvider {
829850
const trace = `Line ${i++} emitted with latency ${fetchRequestStopWatch.elapsed()} ms`;
830851
tracer.trace(trace);
831852

832-
yield responseOpts.shouldRemoveCursorTagFromResponse
853+
yield shouldRemoveCursorTagFromResponse
833854
? v.replaceAll(PromptTags.CURSOR, '')
834855
: v;
835856
}
836857
})();
837858

859+
return new FetchResult.Lines(linesStream, getFetchFailure, () => responseSoFar, fetchRequestStopWatch);
860+
}
861+
862+
private async *_streamEditsImpl(
863+
request: StatelessNextEditRequest,
864+
editStreamCtx: EditStreamContext,
865+
responseOpts: ResponseOpts,
866+
fetchMetadata: FetchMetadata,
867+
retryState: RetryState.t,
868+
delaySession: DelaySession,
869+
tracing: RequestTracingContext,
870+
cancellationToken: CancellationToken,
871+
fetchCts: CancellationTokenSource,
872+
fetchCancellationToken: CancellationToken,
873+
): EditStreaming {
874+
const { tracer, logContext, telemetry } = tracing;
875+
const { endpoint, messages, clippedTaggedCurrentDoc, editWindowInfo, promptPieces, prediction, originalEditWindow } = editStreamCtx;
876+
const { editWindow, editWindowLines, cursorOriginalLinesOffset, editWindowLineRange } = editWindowInfo;
877+
878+
const targetDocument = request.getActiveDocument().id;
879+
880+
// Phase 1: Fetch lifecycle — initiate HTTP request and produce a clean line stream
881+
const fetchResult = await this._performFetch(
882+
endpoint, messages, prediction, request.headerRequestId,
883+
fetchMetadata, responseOpts.shouldRemoveCursorTagFromResponse,
884+
editWindow, request.documentBeforeEdits,
885+
fetchCancellationToken, tracing,
886+
);
887+
888+
if (fetchResult instanceof FetchResult.ModelNotFound) {
889+
return yield* this.doGetNextEdit(request, delaySession, tracing, cancellationToken, retryState);
890+
}
891+
if (fetchResult instanceof FetchResult.Error) {
892+
return fetchResult.reason;
893+
}
894+
895+
const { linesStream, getFetchFailure, getResponseSoFar, fetchRequestStopWatch } = fetchResult;
896+
897+
// Phase 2: Dispatch to the appropriate response format handler
838898
const isFromCursorJump = retryState instanceof RetryState.Retrying && retryState.reason === 'cursorJump';
839899

840-
// Dispatch to the appropriate response format handler
841900
let parseResult: ResponseParseResult.t;
842901

843902
switch (responseOpts.responseFormat) {
@@ -1019,7 +1078,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
10191078
}
10201079
}
10211080

1022-
logContext.setResponse(responseSoFar);
1081+
logContext.setResponse(getResponseSoFar());
10231082

10241083
for (const singleLineEdit of singleLineEdits) {
10251084
tracer.trace(`extracting edit #${i}: ${singleLineEdit.toString()}`);

0 commit comments

Comments
 (0)