( name: string, config: ScopedLspServerConfig, )
| 88 | * await instance.stop() |
| 89 | */ |
| 90 | export function createLSPServerInstance( |
| 91 | name: string, |
| 92 | config: ScopedLspServerConfig, |
| 93 | ): LSPServerInstance { |
| 94 | // Validate that unimplemented fields are not set |
| 95 | if (config.restartOnCrash !== undefined) { |
| 96 | throw new Error( |
| 97 | `LSP server '${name}': restartOnCrash is not yet implemented. Remove this field from the configuration.`, |
| 98 | ) |
| 99 | } |
| 100 | if (config.shutdownTimeout !== undefined) { |
| 101 | throw new Error( |
| 102 | `LSP server '${name}': shutdownTimeout is not yet implemented. Remove this field from the configuration.`, |
| 103 | ) |
| 104 | } |
| 105 | |
| 106 | // Private state encapsulated via closures. Lazy-require LSPClient so |
| 107 | // vscode-jsonrpc (~129KB) only loads when an LSP server is actually |
| 108 | // instantiated, not when the static import chain reaches this module. |
| 109 | // eslint-disable-next-line @typescript-eslint/no-require-imports |
| 110 | const { createLSPClient } = require('./LSPClient.js') as { |
| 111 | createLSPClient: typeof createLSPClientType |
| 112 | } |
| 113 | let state: LspServerState = 'stopped' |
| 114 | let startTime: Date | undefined |
| 115 | let lastError: Error | undefined |
| 116 | let restartCount = 0 |
| 117 | let crashRecoveryCount = 0 |
| 118 | // Propagate crash state so ensureServerStarted can restart on next use. |
| 119 | // Without this, state stays 'running' after crash and the server is never |
| 120 | // restarted (zombie state). |
| 121 | const client = createLSPClient(name, error => { |
| 122 | state = 'error' |
| 123 | lastError = error |
| 124 | crashRecoveryCount++ |
| 125 | }) |
| 126 | |
| 127 | /** |
| 128 | * Starts the LSP server and initializes it with workspace information. |
| 129 | * |
| 130 | * If the server is already running or starting, this method returns immediately. |
| 131 | * On failure, sets state to 'error', logs for monitoring, and throws. |
| 132 | * |
| 133 | * @throws {Error} If server fails to start or initialize |
| 134 | */ |
| 135 | async function start(): Promise<void> { |
| 136 | if (state === 'running' || state === 'starting') { |
| 137 | return |
| 138 | } |
| 139 | |
| 140 | // Cap crash-recovery attempts so a persistently crashing server doesn't |
| 141 | // spawn unbounded child processes on every incoming request. |
| 142 | const maxRestarts = config.maxRestarts ?? 3 |
| 143 | if (state === 'error' && crashRecoveryCount > maxRestarts) { |
| 144 | const error = new Error( |
| 145 | `LSP server '${name}' exceeded max crash recovery attempts (${maxRestarts})`, |
| 146 | ) |
| 147 | lastError = error |
no test coverage detected