Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export const API_URLS = {
COMPLETE(version) {
return `${this.server}/api/${version}/compiler/complete`;
},
// TODO(KTL-3773): support multiple Kotlin versions with LSP-based completions
COMPLETE_WEBSOCKET(/* version */) {
return `ws://${this.server}/api/complete`;
},
get VERSIONS() {
return `${this.server}/versions`;
},
Expand Down
155 changes: 155 additions & 0 deletions src/utils/websockets/webSocketConnection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { API_URLS } from '../../config';

const WS_COMPLETIONS_TIMEOUT_DURATION = 10_000;
const MAX_RECONNECT_RETRIES = 5;

const ConnectionState = {
DISCONNECTED: 'disconnected',
CONNECTED: 'connected',
CONNECTING: 'connecting',
};

const state = {
connection: null,
connectionState: ConnectionState.DISCONNECTED,
retryAttempt: 0,
};

const pendingRequest = new Map();
let nextId = 1;
let queuedRequest = null;

export function completionCallback(callback, project, line, ch) {
const id = String(nextId++);

const timeout = setTimeout(() => {
pendingRequest.delete(id);
callback([]);
}, WS_COMPLETIONS_TIMEOUT_DURATION);

pendingRequest.set(id, { callback, timeout });

const files = project['files'];

const message = {
requestId: id,
completionRequest: { files },
line,
ch,
};

if (isConnectionReady()) {
safeSend(state.connection, message);
try {
state.connection.send(JSON.stringify(message));
} catch (error) {
queuedRequest = message;
handleDisconnect();
}
} else {
queuedRequest = message;
connect();
}
}

function connect() {
if (
isConnectionReady() ||
state.connectionState === ConnectionState.CONNECTING
) {
return;
}

state.connectionState = ConnectionState.CONNECTING;

try {
const ws = new WebSocket(API_URLS.COMPLETE_WEBSOCKET());
state.connection = ws;

ws.onopen = () => {
state.connectionState = ConnectionState.CONNECTED;
state.retryAttempt = 0;

if (queuedRequest) safeSend(ws, queuedRequest);
};

ws.onmessage = handleMessage;

ws.onerror = () => {
handleDisconnect(true);
};

ws.onclose = (event) => {
const tryReconnect = event.code !== 1000;
handleDisconnect(tryReconnect);
};
} catch {
handleDisconnect(true);
}
return true;
}

function handleMessage(msg) {
let payload;
try {
payload = JSON.parse(msg.data);
} catch {
return;
}

if (payload.requestId) {
const entry = pendingRequest.get(payload.requestId);
if (!entry) return;

clearTimeout(entry.timeout);
pendingRequest.delete(payload.requestId);

if (payload.completions) {
entry.callback(payload.completions);
} else {
console.error(payload.message);
entry.callback([]);
}
}
}

function handleDisconnect(tryReconnect) {
if (isConnectionReady()) return;
state.connectionState = ConnectionState.DISCONNECTED;
pendingRequest.forEach((_, entry) => {
entry.callback([]);
clearTimeout(entry.timeout);
});

if (tryReconnect && state.retryAttempt <= MAX_RECONNECT_RETRIES) {
state.retryAttempt++;
connect();
} else {
try {
state.connection.close();
} finally {
state.connection = null;
state.retryAttempt = 0;
}
}
}

function safeSend(ws, message) {
try {
ws.send(JSON.stringify(message));
if (queuedRequest && queuedRequest.requestId === message.requestId)
queuedRequest = null;
} catch (err) {
console.warn(`Failed to send ${message}: `, err);
queuedRequest = message;
handleDisconnect(true);
}
}

function isConnectionReady() {
return (
state.connection &&
state.connectionState === ConnectionState.CONNECTED &&
state.connection.readyState === WebSocket.OPEN
);
}
39 changes: 39 additions & 0 deletions src/webdemo-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
processJUnitResults,
processJVMOutput,
} from './view/output-view';
import { completionCallback } from './utils/websockets/webSocketConnection';

/**
* @typedef {Object} KotlinVersion
Expand Down Expand Up @@ -225,6 +226,44 @@ export default class WebDemoApi {
});
}

/**
* Request for getting list of different completion proposals through WebSockets
*
* Note: currently `compilerVersion` is not used as it is not supported by lsp-based
* completions. Please refer to KTL-3773.
*
* @param code - string code
* @param cursor - cursor position in code
* @param compilerVersion - string kotlin compiler
* @param hiddenDependencies - read only additional files
* @param platform - kotlin platform {@see TargetPlatform}
* @param callback
*/
static getWSAutoCompletion(
code,
cursor,
compilerVersion,
platform,
hiddenDependencies,
callback,
) {
const { line, ch, ...options } = cursor;
const files = [buildFileObject(code, DEFAULT_FILE_NAME)].concat(
hiddenDependencies.map((file, index) =>
buildFileObject(file, `hiddenDependency${index}.kt`),
),
);

const project = {
args: '',
files,
confType: platform.id,
...(options || {}),
};

completionCallback(callback, project, line, ch);
}

/**
* Request for getting errors of current file
*
Expand Down