feat: enhance completion service with session management and language support
- Introduced session management using Redis for tracking active sessions. - Added session claiming and releasing functionality in the completion manager. - Enhanced HTTP and WebSocket completion endpoints to support multiple languages. - Implemented request timeout and maximum body size configurations for API routes. - Updated client-side code to handle session IDs and language parameters in completion requests. - Improved error handling for unsupported languages and session conflicts. - Added tests for the completion manager to ensure proper session handling and cleanup.
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
import type { CompletionRequest, CompletionResponse } from '../types/completion'
|
||||
|
||||
const COMPLETION_API_URL =
|
||||
const COMPLETION_API_BASE_URL =
|
||||
import.meta.env.VITE_COMPLETION_API_URL ??
|
||||
'http://127.0.0.1:8080/api/v1/completions/go'
|
||||
'http://127.0.0.1:8080/api/v1/completions'
|
||||
const COMPLETION_WS_URL =
|
||||
import.meta.env.VITE_COMPLETION_WS_URL ??
|
||||
'ws://127.0.0.1:8080/ws/completions/go'
|
||||
'ws://127.0.0.1:8080/ws/completions'
|
||||
|
||||
const WS_TIMEOUT_MS = 1800
|
||||
|
||||
interface WSCompletionResponse extends CompletionResponse {
|
||||
id: string
|
||||
error?: string
|
||||
routeTo?: string
|
||||
ownerId?: string
|
||||
}
|
||||
|
||||
interface PendingRequest {
|
||||
request: CompletionRequest
|
||||
resolve: (value: CompletionResponse) => void
|
||||
timer: number
|
||||
}
|
||||
@@ -41,14 +44,35 @@ export async function fetchCompletions(
|
||||
|
||||
async function fetchCompletionsByHTTP(
|
||||
request: CompletionRequest,
|
||||
overrideBaseURL?: string,
|
||||
rerouteDepth = 0,
|
||||
): Promise<CompletionResponse> {
|
||||
try {
|
||||
const response = await fetch(COMPLETION_API_URL, {
|
||||
const language = encodeURIComponent(request.language)
|
||||
const baseURL = overrideBaseURL || COMPLETION_API_BASE_URL
|
||||
const response = await fetch(`${baseURL}/${language}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
|
||||
if (response.status === 409) {
|
||||
let rerouteURL = ''
|
||||
try {
|
||||
const body = (await response.json()) as { routeTo?: string }
|
||||
rerouteURL = body.routeTo ?? ''
|
||||
} catch {
|
||||
rerouteURL = ''
|
||||
}
|
||||
|
||||
if (rerouteURL && rerouteDepth < 1) {
|
||||
const normalizedBase = rerouteURL.endsWith('/')
|
||||
? `${rerouteURL}api/v1/completions`
|
||||
: `${rerouteURL}/api/v1/completions`
|
||||
return await fetchCompletionsByHTTP(request, normalizedBase, rerouteDepth + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Completion API error:', response.status)
|
||||
return { items: [], isIncomplete: false }
|
||||
@@ -73,7 +97,7 @@ async function fetchCompletionsByWS(
|
||||
resolve(await fetchCompletionsByHTTP(request))
|
||||
}, WS_TIMEOUT_MS)
|
||||
|
||||
pending.set(id, { resolve, timer })
|
||||
pending.set(id, { request, resolve, timer })
|
||||
|
||||
try {
|
||||
socket.send(JSON.stringify({ id, ...request }))
|
||||
@@ -123,6 +147,13 @@ async function getWebSocket(): Promise<WebSocket> {
|
||||
pending.delete(payload.id)
|
||||
|
||||
if (payload.error) {
|
||||
if (payload.routeTo) {
|
||||
const routeBase = payload.routeTo.endsWith('/')
|
||||
? `${payload.routeTo}api/v1/completions`
|
||||
: `${payload.routeTo}/api/v1/completions`
|
||||
void fetchCompletionsByHTTP(entry.request, routeBase).then(entry.resolve)
|
||||
return
|
||||
}
|
||||
entry.resolve({ items: [], isIncomplete: false })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ const emit = defineEmits<{
|
||||
const editorContainer = ref<HTMLDivElement>()
|
||||
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor>()
|
||||
let completionDisposable: monaco.IDisposable | null = null
|
||||
const completionSessionID = `monaco-${Date.now().toString(36)}`
|
||||
|
||||
const lspEnabledLanguages = new Set(['go', 'javascript', 'typescript'])
|
||||
|
||||
/** Map LSP CompletionItemKind to Monaco CompletionItemKind */
|
||||
function mapKind(kind?: number): monaco.languages.CompletionItemKind {
|
||||
@@ -82,7 +85,13 @@ function mapKind(kind?: number): monaco.languages.CompletionItemKind {
|
||||
}
|
||||
|
||||
function getDocumentURI(language: string): string {
|
||||
const name = `main.${language}`
|
||||
const extensionByLanguage: Record<string, string> = {
|
||||
go: 'go',
|
||||
javascript: 'js',
|
||||
typescript: 'ts',
|
||||
}
|
||||
const extension = extensionByLanguage[language] ?? language
|
||||
const name = `main.${extension}`
|
||||
return `file:///${name}`
|
||||
}
|
||||
|
||||
@@ -91,8 +100,7 @@ function registerCompletionProvider(language: string) {
|
||||
completionDisposable?.dispose()
|
||||
completionDisposable = null
|
||||
|
||||
// Currently only Go completion is wired to backend gopls.
|
||||
if (language !== 'go') {
|
||||
if (!lspEnabledLanguages.has(language)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +108,7 @@ function registerCompletionProvider(language: string) {
|
||||
triggerCharacters: ['.', ':', '('],
|
||||
|
||||
async provideCompletionItems(model, position) {
|
||||
if (model.getLanguageId() !== 'go') {
|
||||
if (!lspEnabledLanguages.has(model.getLanguageId())) {
|
||||
return { suggestions: [] }
|
||||
}
|
||||
|
||||
@@ -108,6 +116,8 @@ function registerCompletionProvider(language: string) {
|
||||
const word = model.getWordUntilPosition(position)
|
||||
|
||||
const response = await fetchCompletions({
|
||||
language,
|
||||
sessionId: completionSessionID,
|
||||
uri: getDocumentURI(language),
|
||||
text: code,
|
||||
line: Math.max(position.lineNumber - 1, 0),
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/** Types for the code completion API */
|
||||
|
||||
export interface CompletionRequest {
|
||||
/** Language key (go/javascript/typescript) */
|
||||
language: string
|
||||
/** Logical client session id */
|
||||
sessionId?: string
|
||||
/** Document URI (file://...) */
|
||||
uri: string
|
||||
/** The full text content of the editor */
|
||||
|
||||
Reference in New Issue
Block a user