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:
2026-02-15 16:22:01 +08:00
parent 23decb8687
commit 57afb90bc0
14 changed files with 1334 additions and 138 deletions

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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 */