1212 lines
50 KiB
JavaScript
1212 lines
50 KiB
JavaScript
"use strict";
|
|
/* --------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
* ------------------------------------------------------------------------------------------ */
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.createMessageConnection = exports.ConnectionOptions = exports.MessageStrategy = exports.CancellationStrategy = exports.CancellationSenderStrategy = exports.CancellationReceiverStrategy = exports.RequestCancellationReceiverStrategy = exports.IdCancellationReceiverStrategy = exports.ConnectionStrategy = exports.ConnectionError = exports.ConnectionErrors = exports.LogTraceNotification = exports.SetTraceNotification = exports.TraceFormat = exports.TraceValues = exports.Trace = exports.NullLogger = exports.ProgressType = exports.ProgressToken = void 0;
|
|
const ral_1 = require("./ral");
|
|
const Is = require("./is");
|
|
const messages_1 = require("./messages");
|
|
const linkedMap_1 = require("./linkedMap");
|
|
const events_1 = require("./events");
|
|
const cancellation_1 = require("./cancellation");
|
|
var CancelNotification;
|
|
(function (CancelNotification) {
|
|
CancelNotification.type = new messages_1.NotificationType('$/cancelRequest');
|
|
})(CancelNotification || (CancelNotification = {}));
|
|
var ProgressToken;
|
|
(function (ProgressToken) {
|
|
function is(value) {
|
|
return typeof value === 'string' || typeof value === 'number';
|
|
}
|
|
ProgressToken.is = is;
|
|
})(ProgressToken = exports.ProgressToken || (exports.ProgressToken = {}));
|
|
var ProgressNotification;
|
|
(function (ProgressNotification) {
|
|
ProgressNotification.type = new messages_1.NotificationType('$/progress');
|
|
})(ProgressNotification || (ProgressNotification = {}));
|
|
class ProgressType {
|
|
constructor() {
|
|
}
|
|
}
|
|
exports.ProgressType = ProgressType;
|
|
var StarRequestHandler;
|
|
(function (StarRequestHandler) {
|
|
function is(value) {
|
|
return Is.func(value);
|
|
}
|
|
StarRequestHandler.is = is;
|
|
})(StarRequestHandler || (StarRequestHandler = {}));
|
|
exports.NullLogger = Object.freeze({
|
|
error: () => { },
|
|
warn: () => { },
|
|
info: () => { },
|
|
log: () => { }
|
|
});
|
|
var Trace;
|
|
(function (Trace) {
|
|
Trace[Trace["Off"] = 0] = "Off";
|
|
Trace[Trace["Messages"] = 1] = "Messages";
|
|
Trace[Trace["Compact"] = 2] = "Compact";
|
|
Trace[Trace["Verbose"] = 3] = "Verbose";
|
|
})(Trace = exports.Trace || (exports.Trace = {}));
|
|
var TraceValues;
|
|
(function (TraceValues) {
|
|
/**
|
|
* Turn tracing off.
|
|
*/
|
|
TraceValues.Off = 'off';
|
|
/**
|
|
* Trace messages only.
|
|
*/
|
|
TraceValues.Messages = 'messages';
|
|
/**
|
|
* Compact message tracing.
|
|
*/
|
|
TraceValues.Compact = 'compact';
|
|
/**
|
|
* Verbose message tracing.
|
|
*/
|
|
TraceValues.Verbose = 'verbose';
|
|
})(TraceValues = exports.TraceValues || (exports.TraceValues = {}));
|
|
(function (Trace) {
|
|
function fromString(value) {
|
|
if (!Is.string(value)) {
|
|
return Trace.Off;
|
|
}
|
|
value = value.toLowerCase();
|
|
switch (value) {
|
|
case 'off':
|
|
return Trace.Off;
|
|
case 'messages':
|
|
return Trace.Messages;
|
|
case 'compact':
|
|
return Trace.Compact;
|
|
case 'verbose':
|
|
return Trace.Verbose;
|
|
default:
|
|
return Trace.Off;
|
|
}
|
|
}
|
|
Trace.fromString = fromString;
|
|
function toString(value) {
|
|
switch (value) {
|
|
case Trace.Off:
|
|
return 'off';
|
|
case Trace.Messages:
|
|
return 'messages';
|
|
case Trace.Compact:
|
|
return 'compact';
|
|
case Trace.Verbose:
|
|
return 'verbose';
|
|
default:
|
|
return 'off';
|
|
}
|
|
}
|
|
Trace.toString = toString;
|
|
})(Trace = exports.Trace || (exports.Trace = {}));
|
|
var TraceFormat;
|
|
(function (TraceFormat) {
|
|
TraceFormat["Text"] = "text";
|
|
TraceFormat["JSON"] = "json";
|
|
})(TraceFormat = exports.TraceFormat || (exports.TraceFormat = {}));
|
|
(function (TraceFormat) {
|
|
function fromString(value) {
|
|
if (!Is.string(value)) {
|
|
return TraceFormat.Text;
|
|
}
|
|
value = value.toLowerCase();
|
|
if (value === 'json') {
|
|
return TraceFormat.JSON;
|
|
}
|
|
else {
|
|
return TraceFormat.Text;
|
|
}
|
|
}
|
|
TraceFormat.fromString = fromString;
|
|
})(TraceFormat = exports.TraceFormat || (exports.TraceFormat = {}));
|
|
var SetTraceNotification;
|
|
(function (SetTraceNotification) {
|
|
SetTraceNotification.type = new messages_1.NotificationType('$/setTrace');
|
|
})(SetTraceNotification = exports.SetTraceNotification || (exports.SetTraceNotification = {}));
|
|
var LogTraceNotification;
|
|
(function (LogTraceNotification) {
|
|
LogTraceNotification.type = new messages_1.NotificationType('$/logTrace');
|
|
})(LogTraceNotification = exports.LogTraceNotification || (exports.LogTraceNotification = {}));
|
|
var ConnectionErrors;
|
|
(function (ConnectionErrors) {
|
|
/**
|
|
* The connection is closed.
|
|
*/
|
|
ConnectionErrors[ConnectionErrors["Closed"] = 1] = "Closed";
|
|
/**
|
|
* The connection got disposed.
|
|
*/
|
|
ConnectionErrors[ConnectionErrors["Disposed"] = 2] = "Disposed";
|
|
/**
|
|
* The connection is already in listening mode.
|
|
*/
|
|
ConnectionErrors[ConnectionErrors["AlreadyListening"] = 3] = "AlreadyListening";
|
|
})(ConnectionErrors = exports.ConnectionErrors || (exports.ConnectionErrors = {}));
|
|
class ConnectionError extends Error {
|
|
constructor(code, message) {
|
|
super(message);
|
|
this.code = code;
|
|
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
}
|
|
}
|
|
exports.ConnectionError = ConnectionError;
|
|
var ConnectionStrategy;
|
|
(function (ConnectionStrategy) {
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && Is.func(candidate.cancelUndispatched);
|
|
}
|
|
ConnectionStrategy.is = is;
|
|
})(ConnectionStrategy = exports.ConnectionStrategy || (exports.ConnectionStrategy = {}));
|
|
var IdCancellationReceiverStrategy;
|
|
(function (IdCancellationReceiverStrategy) {
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && (candidate.kind === undefined || candidate.kind === 'id') && Is.func(candidate.createCancellationTokenSource) && (candidate.dispose === undefined || Is.func(candidate.dispose));
|
|
}
|
|
IdCancellationReceiverStrategy.is = is;
|
|
})(IdCancellationReceiverStrategy = exports.IdCancellationReceiverStrategy || (exports.IdCancellationReceiverStrategy = {}));
|
|
var RequestCancellationReceiverStrategy;
|
|
(function (RequestCancellationReceiverStrategy) {
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && candidate.kind === 'request' && Is.func(candidate.createCancellationTokenSource) && (candidate.dispose === undefined || Is.func(candidate.dispose));
|
|
}
|
|
RequestCancellationReceiverStrategy.is = is;
|
|
})(RequestCancellationReceiverStrategy = exports.RequestCancellationReceiverStrategy || (exports.RequestCancellationReceiverStrategy = {}));
|
|
var CancellationReceiverStrategy;
|
|
(function (CancellationReceiverStrategy) {
|
|
CancellationReceiverStrategy.Message = Object.freeze({
|
|
createCancellationTokenSource(_) {
|
|
return new cancellation_1.CancellationTokenSource();
|
|
}
|
|
});
|
|
function is(value) {
|
|
return IdCancellationReceiverStrategy.is(value) || RequestCancellationReceiverStrategy.is(value);
|
|
}
|
|
CancellationReceiverStrategy.is = is;
|
|
})(CancellationReceiverStrategy = exports.CancellationReceiverStrategy || (exports.CancellationReceiverStrategy = {}));
|
|
var CancellationSenderStrategy;
|
|
(function (CancellationSenderStrategy) {
|
|
CancellationSenderStrategy.Message = Object.freeze({
|
|
sendCancellation(conn, id) {
|
|
return conn.sendNotification(CancelNotification.type, { id });
|
|
},
|
|
cleanup(_) { }
|
|
});
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && Is.func(candidate.sendCancellation) && Is.func(candidate.cleanup);
|
|
}
|
|
CancellationSenderStrategy.is = is;
|
|
})(CancellationSenderStrategy = exports.CancellationSenderStrategy || (exports.CancellationSenderStrategy = {}));
|
|
var CancellationStrategy;
|
|
(function (CancellationStrategy) {
|
|
CancellationStrategy.Message = Object.freeze({
|
|
receiver: CancellationReceiverStrategy.Message,
|
|
sender: CancellationSenderStrategy.Message
|
|
});
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && CancellationReceiverStrategy.is(candidate.receiver) && CancellationSenderStrategy.is(candidate.sender);
|
|
}
|
|
CancellationStrategy.is = is;
|
|
})(CancellationStrategy = exports.CancellationStrategy || (exports.CancellationStrategy = {}));
|
|
var MessageStrategy;
|
|
(function (MessageStrategy) {
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && Is.func(candidate.handleMessage);
|
|
}
|
|
MessageStrategy.is = is;
|
|
})(MessageStrategy = exports.MessageStrategy || (exports.MessageStrategy = {}));
|
|
var ConnectionOptions;
|
|
(function (ConnectionOptions) {
|
|
function is(value) {
|
|
const candidate = value;
|
|
return candidate && (CancellationStrategy.is(candidate.cancellationStrategy) || ConnectionStrategy.is(candidate.connectionStrategy) || MessageStrategy.is(candidate.messageStrategy));
|
|
}
|
|
ConnectionOptions.is = is;
|
|
})(ConnectionOptions = exports.ConnectionOptions || (exports.ConnectionOptions = {}));
|
|
var ConnectionState;
|
|
(function (ConnectionState) {
|
|
ConnectionState[ConnectionState["New"] = 1] = "New";
|
|
ConnectionState[ConnectionState["Listening"] = 2] = "Listening";
|
|
ConnectionState[ConnectionState["Closed"] = 3] = "Closed";
|
|
ConnectionState[ConnectionState["Disposed"] = 4] = "Disposed";
|
|
})(ConnectionState || (ConnectionState = {}));
|
|
function createMessageConnection(messageReader, messageWriter, _logger, options) {
|
|
const logger = _logger !== undefined ? _logger : exports.NullLogger;
|
|
let sequenceNumber = 0;
|
|
let notificationSequenceNumber = 0;
|
|
let unknownResponseSequenceNumber = 0;
|
|
const version = '2.0';
|
|
let starRequestHandler = undefined;
|
|
const requestHandlers = new Map();
|
|
let starNotificationHandler = undefined;
|
|
const notificationHandlers = new Map();
|
|
const progressHandlers = new Map();
|
|
let timer;
|
|
let messageQueue = new linkedMap_1.LinkedMap();
|
|
let responsePromises = new Map();
|
|
let knownCanceledRequests = new Set();
|
|
let requestTokens = new Map();
|
|
let trace = Trace.Off;
|
|
let traceFormat = TraceFormat.Text;
|
|
let tracer;
|
|
let state = ConnectionState.New;
|
|
const errorEmitter = new events_1.Emitter();
|
|
const closeEmitter = new events_1.Emitter();
|
|
const unhandledNotificationEmitter = new events_1.Emitter();
|
|
const unhandledProgressEmitter = new events_1.Emitter();
|
|
const disposeEmitter = new events_1.Emitter();
|
|
const cancellationStrategy = (options && options.cancellationStrategy) ? options.cancellationStrategy : CancellationStrategy.Message;
|
|
function createRequestQueueKey(id) {
|
|
if (id === null) {
|
|
throw new Error(`Can't send requests with id null since the response can't be correlated.`);
|
|
}
|
|
return 'req-' + id.toString();
|
|
}
|
|
function createResponseQueueKey(id) {
|
|
if (id === null) {
|
|
return 'res-unknown-' + (++unknownResponseSequenceNumber).toString();
|
|
}
|
|
else {
|
|
return 'res-' + id.toString();
|
|
}
|
|
}
|
|
function createNotificationQueueKey() {
|
|
return 'not-' + (++notificationSequenceNumber).toString();
|
|
}
|
|
function addMessageToQueue(queue, message) {
|
|
if (messages_1.Message.isRequest(message)) {
|
|
queue.set(createRequestQueueKey(message.id), message);
|
|
}
|
|
else if (messages_1.Message.isResponse(message)) {
|
|
queue.set(createResponseQueueKey(message.id), message);
|
|
}
|
|
else {
|
|
queue.set(createNotificationQueueKey(), message);
|
|
}
|
|
}
|
|
function cancelUndispatched(_message) {
|
|
return undefined;
|
|
}
|
|
function isListening() {
|
|
return state === ConnectionState.Listening;
|
|
}
|
|
function isClosed() {
|
|
return state === ConnectionState.Closed;
|
|
}
|
|
function isDisposed() {
|
|
return state === ConnectionState.Disposed;
|
|
}
|
|
function closeHandler() {
|
|
if (state === ConnectionState.New || state === ConnectionState.Listening) {
|
|
state = ConnectionState.Closed;
|
|
closeEmitter.fire(undefined);
|
|
}
|
|
// If the connection is disposed don't sent close events.
|
|
}
|
|
function readErrorHandler(error) {
|
|
errorEmitter.fire([error, undefined, undefined]);
|
|
}
|
|
function writeErrorHandler(data) {
|
|
errorEmitter.fire(data);
|
|
}
|
|
messageReader.onClose(closeHandler);
|
|
messageReader.onError(readErrorHandler);
|
|
messageWriter.onClose(closeHandler);
|
|
messageWriter.onError(writeErrorHandler);
|
|
function triggerMessageQueue() {
|
|
if (timer || messageQueue.size === 0) {
|
|
return;
|
|
}
|
|
timer = (0, ral_1.default)().timer.setImmediate(() => {
|
|
timer = undefined;
|
|
processMessageQueue();
|
|
});
|
|
}
|
|
function handleMessage(message) {
|
|
if (messages_1.Message.isRequest(message)) {
|
|
handleRequest(message);
|
|
}
|
|
else if (messages_1.Message.isNotification(message)) {
|
|
handleNotification(message);
|
|
}
|
|
else if (messages_1.Message.isResponse(message)) {
|
|
handleResponse(message);
|
|
}
|
|
else {
|
|
handleInvalidMessage(message);
|
|
}
|
|
}
|
|
function processMessageQueue() {
|
|
if (messageQueue.size === 0) {
|
|
return;
|
|
}
|
|
const message = messageQueue.shift();
|
|
try {
|
|
const messageStrategy = options?.messageStrategy;
|
|
if (MessageStrategy.is(messageStrategy)) {
|
|
messageStrategy.handleMessage(message, handleMessage);
|
|
}
|
|
else {
|
|
handleMessage(message);
|
|
}
|
|
}
|
|
finally {
|
|
triggerMessageQueue();
|
|
}
|
|
}
|
|
const callback = (message) => {
|
|
try {
|
|
// We have received a cancellation message. Check if the message is still in the queue
|
|
// and cancel it if allowed to do so.
|
|
if (messages_1.Message.isNotification(message) && message.method === CancelNotification.type.method) {
|
|
const cancelId = message.params.id;
|
|
const key = createRequestQueueKey(cancelId);
|
|
const toCancel = messageQueue.get(key);
|
|
if (messages_1.Message.isRequest(toCancel)) {
|
|
const strategy = options?.connectionStrategy;
|
|
const response = (strategy && strategy.cancelUndispatched) ? strategy.cancelUndispatched(toCancel, cancelUndispatched) : cancelUndispatched(toCancel);
|
|
if (response && (response.error !== undefined || response.result !== undefined)) {
|
|
messageQueue.delete(key);
|
|
requestTokens.delete(cancelId);
|
|
response.id = toCancel.id;
|
|
traceSendingResponse(response, message.method, Date.now());
|
|
messageWriter.write(response).catch(() => logger.error(`Sending response for canceled message failed.`));
|
|
return;
|
|
}
|
|
}
|
|
const cancellationToken = requestTokens.get(cancelId);
|
|
// The request is already running. Cancel the token
|
|
if (cancellationToken !== undefined) {
|
|
cancellationToken.cancel();
|
|
traceReceivedNotification(message);
|
|
return;
|
|
}
|
|
else {
|
|
// Remember the cancel but still queue the message to
|
|
// clean up state in process message.
|
|
knownCanceledRequests.add(cancelId);
|
|
}
|
|
}
|
|
addMessageToQueue(messageQueue, message);
|
|
}
|
|
finally {
|
|
triggerMessageQueue();
|
|
}
|
|
};
|
|
function handleRequest(requestMessage) {
|
|
if (isDisposed()) {
|
|
// we return here silently since we fired an event when the
|
|
// connection got disposed.
|
|
return;
|
|
}
|
|
function reply(resultOrError, method, startTime) {
|
|
const message = {
|
|
jsonrpc: version,
|
|
id: requestMessage.id
|
|
};
|
|
if (resultOrError instanceof messages_1.ResponseError) {
|
|
message.error = resultOrError.toJson();
|
|
}
|
|
else {
|
|
message.result = resultOrError === undefined ? null : resultOrError;
|
|
}
|
|
traceSendingResponse(message, method, startTime);
|
|
messageWriter.write(message).catch(() => logger.error(`Sending response failed.`));
|
|
}
|
|
function replyError(error, method, startTime) {
|
|
const message = {
|
|
jsonrpc: version,
|
|
id: requestMessage.id,
|
|
error: error.toJson()
|
|
};
|
|
traceSendingResponse(message, method, startTime);
|
|
messageWriter.write(message).catch(() => logger.error(`Sending response failed.`));
|
|
}
|
|
function replySuccess(result, method, startTime) {
|
|
// The JSON RPC defines that a response must either have a result or an error
|
|
// So we can't treat undefined as a valid response result.
|
|
if (result === undefined) {
|
|
result = null;
|
|
}
|
|
const message = {
|
|
jsonrpc: version,
|
|
id: requestMessage.id,
|
|
result: result
|
|
};
|
|
traceSendingResponse(message, method, startTime);
|
|
messageWriter.write(message).catch(() => logger.error(`Sending response failed.`));
|
|
}
|
|
traceReceivedRequest(requestMessage);
|
|
const element = requestHandlers.get(requestMessage.method);
|
|
let type;
|
|
let requestHandler;
|
|
if (element) {
|
|
type = element.type;
|
|
requestHandler = element.handler;
|
|
}
|
|
const startTime = Date.now();
|
|
if (requestHandler || starRequestHandler) {
|
|
const tokenKey = requestMessage.id ?? String(Date.now()); //
|
|
const cancellationSource = IdCancellationReceiverStrategy.is(cancellationStrategy.receiver)
|
|
? cancellationStrategy.receiver.createCancellationTokenSource(tokenKey)
|
|
: cancellationStrategy.receiver.createCancellationTokenSource(requestMessage);
|
|
if (requestMessage.id !== null && knownCanceledRequests.has(requestMessage.id)) {
|
|
cancellationSource.cancel();
|
|
}
|
|
if (requestMessage.id !== null) {
|
|
requestTokens.set(tokenKey, cancellationSource);
|
|
}
|
|
try {
|
|
let handlerResult;
|
|
if (requestHandler) {
|
|
if (requestMessage.params === undefined) {
|
|
if (type !== undefined && type.numberOfParams !== 0) {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InvalidParams, `Request ${requestMessage.method} defines ${type.numberOfParams} params but received none.`), requestMessage.method, startTime);
|
|
return;
|
|
}
|
|
handlerResult = requestHandler(cancellationSource.token);
|
|
}
|
|
else if (Array.isArray(requestMessage.params)) {
|
|
if (type !== undefined && type.parameterStructures === messages_1.ParameterStructures.byName) {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InvalidParams, `Request ${requestMessage.method} defines parameters by name but received parameters by position`), requestMessage.method, startTime);
|
|
return;
|
|
}
|
|
handlerResult = requestHandler(...requestMessage.params, cancellationSource.token);
|
|
}
|
|
else {
|
|
if (type !== undefined && type.parameterStructures === messages_1.ParameterStructures.byPosition) {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InvalidParams, `Request ${requestMessage.method} defines parameters by position but received parameters by name`), requestMessage.method, startTime);
|
|
return;
|
|
}
|
|
handlerResult = requestHandler(requestMessage.params, cancellationSource.token);
|
|
}
|
|
}
|
|
else if (starRequestHandler) {
|
|
handlerResult = starRequestHandler(requestMessage.method, requestMessage.params, cancellationSource.token);
|
|
}
|
|
const promise = handlerResult;
|
|
if (!handlerResult) {
|
|
requestTokens.delete(tokenKey);
|
|
replySuccess(handlerResult, requestMessage.method, startTime);
|
|
}
|
|
else if (promise.then) {
|
|
promise.then((resultOrError) => {
|
|
requestTokens.delete(tokenKey);
|
|
reply(resultOrError, requestMessage.method, startTime);
|
|
}, error => {
|
|
requestTokens.delete(tokenKey);
|
|
if (error instanceof messages_1.ResponseError) {
|
|
replyError(error, requestMessage.method, startTime);
|
|
}
|
|
else if (error && Is.string(error.message)) {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, `Request ${requestMessage.method} failed with message: ${error.message}`), requestMessage.method, startTime);
|
|
}
|
|
else {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, `Request ${requestMessage.method} failed unexpectedly without providing any details.`), requestMessage.method, startTime);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
requestTokens.delete(tokenKey);
|
|
reply(handlerResult, requestMessage.method, startTime);
|
|
}
|
|
}
|
|
catch (error) {
|
|
requestTokens.delete(tokenKey);
|
|
if (error instanceof messages_1.ResponseError) {
|
|
reply(error, requestMessage.method, startTime);
|
|
}
|
|
else if (error && Is.string(error.message)) {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, `Request ${requestMessage.method} failed with message: ${error.message}`), requestMessage.method, startTime);
|
|
}
|
|
else {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.InternalError, `Request ${requestMessage.method} failed unexpectedly without providing any details.`), requestMessage.method, startTime);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
replyError(new messages_1.ResponseError(messages_1.ErrorCodes.MethodNotFound, `Unhandled method ${requestMessage.method}`), requestMessage.method, startTime);
|
|
}
|
|
}
|
|
function handleResponse(responseMessage) {
|
|
if (isDisposed()) {
|
|
// See handle request.
|
|
return;
|
|
}
|
|
if (responseMessage.id === null) {
|
|
if (responseMessage.error) {
|
|
logger.error(`Received response message without id: Error is: \n${JSON.stringify(responseMessage.error, undefined, 4)}`);
|
|
}
|
|
else {
|
|
logger.error(`Received response message without id. No further error information provided.`);
|
|
}
|
|
}
|
|
else {
|
|
const key = responseMessage.id;
|
|
const responsePromise = responsePromises.get(key);
|
|
traceReceivedResponse(responseMessage, responsePromise);
|
|
if (responsePromise !== undefined) {
|
|
responsePromises.delete(key);
|
|
try {
|
|
if (responseMessage.error) {
|
|
const error = responseMessage.error;
|
|
responsePromise.reject(new messages_1.ResponseError(error.code, error.message, error.data));
|
|
}
|
|
else if (responseMessage.result !== undefined) {
|
|
responsePromise.resolve(responseMessage.result);
|
|
}
|
|
else {
|
|
throw new Error('Should never happen.');
|
|
}
|
|
}
|
|
catch (error) {
|
|
if (error.message) {
|
|
logger.error(`Response handler '${responsePromise.method}' failed with message: ${error.message}`);
|
|
}
|
|
else {
|
|
logger.error(`Response handler '${responsePromise.method}' failed unexpectedly.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function handleNotification(message) {
|
|
if (isDisposed()) {
|
|
// See handle request.
|
|
return;
|
|
}
|
|
let type = undefined;
|
|
let notificationHandler;
|
|
if (message.method === CancelNotification.type.method) {
|
|
const cancelId = message.params.id;
|
|
knownCanceledRequests.delete(cancelId);
|
|
traceReceivedNotification(message);
|
|
return;
|
|
}
|
|
else {
|
|
const element = notificationHandlers.get(message.method);
|
|
if (element) {
|
|
notificationHandler = element.handler;
|
|
type = element.type;
|
|
}
|
|
}
|
|
if (notificationHandler || starNotificationHandler) {
|
|
try {
|
|
traceReceivedNotification(message);
|
|
if (notificationHandler) {
|
|
if (message.params === undefined) {
|
|
if (type !== undefined) {
|
|
if (type.numberOfParams !== 0 && type.parameterStructures !== messages_1.ParameterStructures.byName) {
|
|
logger.error(`Notification ${message.method} defines ${type.numberOfParams} params but received none.`);
|
|
}
|
|
}
|
|
notificationHandler();
|
|
}
|
|
else if (Array.isArray(message.params)) {
|
|
// There are JSON-RPC libraries that send progress message as positional params although
|
|
// specified as named. So convert them if this is the case.
|
|
const params = message.params;
|
|
if (message.method === ProgressNotification.type.method && params.length === 2 && ProgressToken.is(params[0])) {
|
|
notificationHandler({ token: params[0], value: params[1] });
|
|
}
|
|
else {
|
|
if (type !== undefined) {
|
|
if (type.parameterStructures === messages_1.ParameterStructures.byName) {
|
|
logger.error(`Notification ${message.method} defines parameters by name but received parameters by position`);
|
|
}
|
|
if (type.numberOfParams !== message.params.length) {
|
|
logger.error(`Notification ${message.method} defines ${type.numberOfParams} params but received ${params.length} arguments`);
|
|
}
|
|
}
|
|
notificationHandler(...params);
|
|
}
|
|
}
|
|
else {
|
|
if (type !== undefined && type.parameterStructures === messages_1.ParameterStructures.byPosition) {
|
|
logger.error(`Notification ${message.method} defines parameters by position but received parameters by name`);
|
|
}
|
|
notificationHandler(message.params);
|
|
}
|
|
}
|
|
else if (starNotificationHandler) {
|
|
starNotificationHandler(message.method, message.params);
|
|
}
|
|
}
|
|
catch (error) {
|
|
if (error.message) {
|
|
logger.error(`Notification handler '${message.method}' failed with message: ${error.message}`);
|
|
}
|
|
else {
|
|
logger.error(`Notification handler '${message.method}' failed unexpectedly.`);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
unhandledNotificationEmitter.fire(message);
|
|
}
|
|
}
|
|
function handleInvalidMessage(message) {
|
|
if (!message) {
|
|
logger.error('Received empty message.');
|
|
return;
|
|
}
|
|
logger.error(`Received message which is neither a response nor a notification message:\n${JSON.stringify(message, null, 4)}`);
|
|
// Test whether we find an id to reject the promise
|
|
const responseMessage = message;
|
|
if (Is.string(responseMessage.id) || Is.number(responseMessage.id)) {
|
|
const key = responseMessage.id;
|
|
const responseHandler = responsePromises.get(key);
|
|
if (responseHandler) {
|
|
responseHandler.reject(new Error('The received response has neither a result nor an error property.'));
|
|
}
|
|
}
|
|
}
|
|
function stringifyTrace(params) {
|
|
if (params === undefined || params === null) {
|
|
return undefined;
|
|
}
|
|
switch (trace) {
|
|
case Trace.Verbose:
|
|
return JSON.stringify(params, null, 4);
|
|
case Trace.Compact:
|
|
return JSON.stringify(params);
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
function traceSendingRequest(message) {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if ((trace === Trace.Verbose || trace === Trace.Compact) && message.params) {
|
|
data = `Params: ${stringifyTrace(message.params)}\n\n`;
|
|
}
|
|
tracer.log(`Sending request '${message.method} - (${message.id})'.`, data);
|
|
}
|
|
else {
|
|
logLSPMessage('send-request', message);
|
|
}
|
|
}
|
|
function traceSendingNotification(message) {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if (trace === Trace.Verbose || trace === Trace.Compact) {
|
|
if (message.params) {
|
|
data = `Params: ${stringifyTrace(message.params)}\n\n`;
|
|
}
|
|
else {
|
|
data = 'No parameters provided.\n\n';
|
|
}
|
|
}
|
|
tracer.log(`Sending notification '${message.method}'.`, data);
|
|
}
|
|
else {
|
|
logLSPMessage('send-notification', message);
|
|
}
|
|
}
|
|
function traceSendingResponse(message, method, startTime) {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if (trace === Trace.Verbose || trace === Trace.Compact) {
|
|
if (message.error && message.error.data) {
|
|
data = `Error data: ${stringifyTrace(message.error.data)}\n\n`;
|
|
}
|
|
else {
|
|
if (message.result) {
|
|
data = `Result: ${stringifyTrace(message.result)}\n\n`;
|
|
}
|
|
else if (message.error === undefined) {
|
|
data = 'No result returned.\n\n';
|
|
}
|
|
}
|
|
}
|
|
tracer.log(`Sending response '${method} - (${message.id})'. Processing request took ${Date.now() - startTime}ms`, data);
|
|
}
|
|
else {
|
|
logLSPMessage('send-response', message);
|
|
}
|
|
}
|
|
function traceReceivedRequest(message) {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if ((trace === Trace.Verbose || trace === Trace.Compact) && message.params) {
|
|
data = `Params: ${stringifyTrace(message.params)}\n\n`;
|
|
}
|
|
tracer.log(`Received request '${message.method} - (${message.id})'.`, data);
|
|
}
|
|
else {
|
|
logLSPMessage('receive-request', message);
|
|
}
|
|
}
|
|
function traceReceivedNotification(message) {
|
|
if (trace === Trace.Off || !tracer || message.method === LogTraceNotification.type.method) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if (trace === Trace.Verbose || trace === Trace.Compact) {
|
|
if (message.params) {
|
|
data = `Params: ${stringifyTrace(message.params)}\n\n`;
|
|
}
|
|
else {
|
|
data = 'No parameters provided.\n\n';
|
|
}
|
|
}
|
|
tracer.log(`Received notification '${message.method}'.`, data);
|
|
}
|
|
else {
|
|
logLSPMessage('receive-notification', message);
|
|
}
|
|
}
|
|
function traceReceivedResponse(message, responsePromise) {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
if (traceFormat === TraceFormat.Text) {
|
|
let data = undefined;
|
|
if (trace === Trace.Verbose || trace === Trace.Compact) {
|
|
if (message.error && message.error.data) {
|
|
data = `Error data: ${stringifyTrace(message.error.data)}\n\n`;
|
|
}
|
|
else {
|
|
if (message.result) {
|
|
data = `Result: ${stringifyTrace(message.result)}\n\n`;
|
|
}
|
|
else if (message.error === undefined) {
|
|
data = 'No result returned.\n\n';
|
|
}
|
|
}
|
|
}
|
|
if (responsePromise) {
|
|
const error = message.error ? ` Request failed: ${message.error.message} (${message.error.code}).` : '';
|
|
tracer.log(`Received response '${responsePromise.method} - (${message.id})' in ${Date.now() - responsePromise.timerStart}ms.${error}`, data);
|
|
}
|
|
else {
|
|
tracer.log(`Received response ${message.id} without active response promise.`, data);
|
|
}
|
|
}
|
|
else {
|
|
logLSPMessage('receive-response', message);
|
|
}
|
|
}
|
|
function logLSPMessage(type, message) {
|
|
if (!tracer || trace === Trace.Off) {
|
|
return;
|
|
}
|
|
const lspMessage = {
|
|
isLSPMessage: true,
|
|
type,
|
|
message,
|
|
timestamp: Date.now()
|
|
};
|
|
tracer.log(lspMessage);
|
|
}
|
|
function throwIfClosedOrDisposed() {
|
|
if (isClosed()) {
|
|
throw new ConnectionError(ConnectionErrors.Closed, 'Connection is closed.');
|
|
}
|
|
if (isDisposed()) {
|
|
throw new ConnectionError(ConnectionErrors.Disposed, 'Connection is disposed.');
|
|
}
|
|
}
|
|
function throwIfListening() {
|
|
if (isListening()) {
|
|
throw new ConnectionError(ConnectionErrors.AlreadyListening, 'Connection is already listening');
|
|
}
|
|
}
|
|
function throwIfNotListening() {
|
|
if (!isListening()) {
|
|
throw new Error('Call listen() first.');
|
|
}
|
|
}
|
|
function undefinedToNull(param) {
|
|
if (param === undefined) {
|
|
return null;
|
|
}
|
|
else {
|
|
return param;
|
|
}
|
|
}
|
|
function nullToUndefined(param) {
|
|
if (param === null) {
|
|
return undefined;
|
|
}
|
|
else {
|
|
return param;
|
|
}
|
|
}
|
|
function isNamedParam(param) {
|
|
return param !== undefined && param !== null && !Array.isArray(param) && typeof param === 'object';
|
|
}
|
|
function computeSingleParam(parameterStructures, param) {
|
|
switch (parameterStructures) {
|
|
case messages_1.ParameterStructures.auto:
|
|
if (isNamedParam(param)) {
|
|
return nullToUndefined(param);
|
|
}
|
|
else {
|
|
return [undefinedToNull(param)];
|
|
}
|
|
case messages_1.ParameterStructures.byName:
|
|
if (!isNamedParam(param)) {
|
|
throw new Error(`Received parameters by name but param is not an object literal.`);
|
|
}
|
|
return nullToUndefined(param);
|
|
case messages_1.ParameterStructures.byPosition:
|
|
return [undefinedToNull(param)];
|
|
default:
|
|
throw new Error(`Unknown parameter structure ${parameterStructures.toString()}`);
|
|
}
|
|
}
|
|
function computeMessageParams(type, params) {
|
|
let result;
|
|
const numberOfParams = type.numberOfParams;
|
|
switch (numberOfParams) {
|
|
case 0:
|
|
result = undefined;
|
|
break;
|
|
case 1:
|
|
result = computeSingleParam(type.parameterStructures, params[0]);
|
|
break;
|
|
default:
|
|
result = [];
|
|
for (let i = 0; i < params.length && i < numberOfParams; i++) {
|
|
result.push(undefinedToNull(params[i]));
|
|
}
|
|
if (params.length < numberOfParams) {
|
|
for (let i = params.length; i < numberOfParams; i++) {
|
|
result.push(null);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
const connection = {
|
|
sendNotification: (type, ...args) => {
|
|
throwIfClosedOrDisposed();
|
|
let method;
|
|
let messageParams;
|
|
if (Is.string(type)) {
|
|
method = type;
|
|
const first = args[0];
|
|
let paramStart = 0;
|
|
let parameterStructures = messages_1.ParameterStructures.auto;
|
|
if (messages_1.ParameterStructures.is(first)) {
|
|
paramStart = 1;
|
|
parameterStructures = first;
|
|
}
|
|
let paramEnd = args.length;
|
|
const numberOfParams = paramEnd - paramStart;
|
|
switch (numberOfParams) {
|
|
case 0:
|
|
messageParams = undefined;
|
|
break;
|
|
case 1:
|
|
messageParams = computeSingleParam(parameterStructures, args[paramStart]);
|
|
break;
|
|
default:
|
|
if (parameterStructures === messages_1.ParameterStructures.byName) {
|
|
throw new Error(`Received ${numberOfParams} parameters for 'by Name' notification parameter structure.`);
|
|
}
|
|
messageParams = args.slice(paramStart, paramEnd).map(value => undefinedToNull(value));
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
const params = args;
|
|
method = type.method;
|
|
messageParams = computeMessageParams(type, params);
|
|
}
|
|
const notificationMessage = {
|
|
jsonrpc: version,
|
|
method: method,
|
|
params: messageParams
|
|
};
|
|
traceSendingNotification(notificationMessage);
|
|
return messageWriter.write(notificationMessage).catch((error) => {
|
|
logger.error(`Sending notification failed.`);
|
|
throw error;
|
|
});
|
|
},
|
|
onNotification: (type, handler) => {
|
|
throwIfClosedOrDisposed();
|
|
let method;
|
|
if (Is.func(type)) {
|
|
starNotificationHandler = type;
|
|
}
|
|
else if (handler) {
|
|
if (Is.string(type)) {
|
|
method = type;
|
|
notificationHandlers.set(type, { type: undefined, handler });
|
|
}
|
|
else {
|
|
method = type.method;
|
|
notificationHandlers.set(type.method, { type, handler });
|
|
}
|
|
}
|
|
return {
|
|
dispose: () => {
|
|
if (method !== undefined) {
|
|
notificationHandlers.delete(method);
|
|
}
|
|
else {
|
|
starNotificationHandler = undefined;
|
|
}
|
|
}
|
|
};
|
|
},
|
|
onProgress: (_type, token, handler) => {
|
|
if (progressHandlers.has(token)) {
|
|
throw new Error(`Progress handler for token ${token} already registered`);
|
|
}
|
|
progressHandlers.set(token, handler);
|
|
return {
|
|
dispose: () => {
|
|
progressHandlers.delete(token);
|
|
}
|
|
};
|
|
},
|
|
sendProgress: (_type, token, value) => {
|
|
// This should not await but simple return to ensure that we don't have another
|
|
// async scheduling. Otherwise one send could overtake another send.
|
|
return connection.sendNotification(ProgressNotification.type, { token, value });
|
|
},
|
|
onUnhandledProgress: unhandledProgressEmitter.event,
|
|
sendRequest: (type, ...args) => {
|
|
throwIfClosedOrDisposed();
|
|
throwIfNotListening();
|
|
let method;
|
|
let messageParams;
|
|
let token = undefined;
|
|
if (Is.string(type)) {
|
|
method = type;
|
|
const first = args[0];
|
|
const last = args[args.length - 1];
|
|
let paramStart = 0;
|
|
let parameterStructures = messages_1.ParameterStructures.auto;
|
|
if (messages_1.ParameterStructures.is(first)) {
|
|
paramStart = 1;
|
|
parameterStructures = first;
|
|
}
|
|
let paramEnd = args.length;
|
|
if (cancellation_1.CancellationToken.is(last)) {
|
|
paramEnd = paramEnd - 1;
|
|
token = last;
|
|
}
|
|
const numberOfParams = paramEnd - paramStart;
|
|
switch (numberOfParams) {
|
|
case 0:
|
|
messageParams = undefined;
|
|
break;
|
|
case 1:
|
|
messageParams = computeSingleParam(parameterStructures, args[paramStart]);
|
|
break;
|
|
default:
|
|
if (parameterStructures === messages_1.ParameterStructures.byName) {
|
|
throw new Error(`Received ${numberOfParams} parameters for 'by Name' request parameter structure.`);
|
|
}
|
|
messageParams = args.slice(paramStart, paramEnd).map(value => undefinedToNull(value));
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
const params = args;
|
|
method = type.method;
|
|
messageParams = computeMessageParams(type, params);
|
|
const numberOfParams = type.numberOfParams;
|
|
token = cancellation_1.CancellationToken.is(params[numberOfParams]) ? params[numberOfParams] : undefined;
|
|
}
|
|
const id = sequenceNumber++;
|
|
let disposable;
|
|
if (token) {
|
|
disposable = token.onCancellationRequested(() => {
|
|
const p = cancellationStrategy.sender.sendCancellation(connection, id);
|
|
if (p === undefined) {
|
|
logger.log(`Received no promise from cancellation strategy when cancelling id ${id}`);
|
|
return Promise.resolve();
|
|
}
|
|
else {
|
|
return p.catch(() => {
|
|
logger.log(`Sending cancellation messages for id ${id} failed`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
const requestMessage = {
|
|
jsonrpc: version,
|
|
id: id,
|
|
method: method,
|
|
params: messageParams
|
|
};
|
|
traceSendingRequest(requestMessage);
|
|
if (typeof cancellationStrategy.sender.enableCancellation === 'function') {
|
|
cancellationStrategy.sender.enableCancellation(requestMessage);
|
|
}
|
|
return new Promise(async (resolve, reject) => {
|
|
const resolveWithCleanup = (r) => {
|
|
resolve(r);
|
|
cancellationStrategy.sender.cleanup(id);
|
|
disposable?.dispose();
|
|
};
|
|
const rejectWithCleanup = (r) => {
|
|
reject(r);
|
|
cancellationStrategy.sender.cleanup(id);
|
|
disposable?.dispose();
|
|
};
|
|
const responsePromise = { method: method, timerStart: Date.now(), resolve: resolveWithCleanup, reject: rejectWithCleanup };
|
|
try {
|
|
await messageWriter.write(requestMessage);
|
|
responsePromises.set(id, responsePromise);
|
|
}
|
|
catch (error) {
|
|
logger.error(`Sending request failed.`);
|
|
// Writing the message failed. So we need to reject the promise.
|
|
responsePromise.reject(new messages_1.ResponseError(messages_1.ErrorCodes.MessageWriteError, error.message ? error.message : 'Unknown reason'));
|
|
throw error;
|
|
}
|
|
});
|
|
},
|
|
onRequest: (type, handler) => {
|
|
throwIfClosedOrDisposed();
|
|
let method = null;
|
|
if (StarRequestHandler.is(type)) {
|
|
method = undefined;
|
|
starRequestHandler = type;
|
|
}
|
|
else if (Is.string(type)) {
|
|
method = null;
|
|
if (handler !== undefined) {
|
|
method = type;
|
|
requestHandlers.set(type, { handler: handler, type: undefined });
|
|
}
|
|
}
|
|
else {
|
|
if (handler !== undefined) {
|
|
method = type.method;
|
|
requestHandlers.set(type.method, { type, handler });
|
|
}
|
|
}
|
|
return {
|
|
dispose: () => {
|
|
if (method === null) {
|
|
return;
|
|
}
|
|
if (method !== undefined) {
|
|
requestHandlers.delete(method);
|
|
}
|
|
else {
|
|
starRequestHandler = undefined;
|
|
}
|
|
}
|
|
};
|
|
},
|
|
hasPendingResponse: () => {
|
|
return responsePromises.size > 0;
|
|
},
|
|
trace: async (_value, _tracer, sendNotificationOrTraceOptions) => {
|
|
let _sendNotification = false;
|
|
let _traceFormat = TraceFormat.Text;
|
|
if (sendNotificationOrTraceOptions !== undefined) {
|
|
if (Is.boolean(sendNotificationOrTraceOptions)) {
|
|
_sendNotification = sendNotificationOrTraceOptions;
|
|
}
|
|
else {
|
|
_sendNotification = sendNotificationOrTraceOptions.sendNotification || false;
|
|
_traceFormat = sendNotificationOrTraceOptions.traceFormat || TraceFormat.Text;
|
|
}
|
|
}
|
|
trace = _value;
|
|
traceFormat = _traceFormat;
|
|
if (trace === Trace.Off) {
|
|
tracer = undefined;
|
|
}
|
|
else {
|
|
tracer = _tracer;
|
|
}
|
|
if (_sendNotification && !isClosed() && !isDisposed()) {
|
|
await connection.sendNotification(SetTraceNotification.type, { value: Trace.toString(_value) });
|
|
}
|
|
},
|
|
onError: errorEmitter.event,
|
|
onClose: closeEmitter.event,
|
|
onUnhandledNotification: unhandledNotificationEmitter.event,
|
|
onDispose: disposeEmitter.event,
|
|
end: () => {
|
|
messageWriter.end();
|
|
},
|
|
dispose: () => {
|
|
if (isDisposed()) {
|
|
return;
|
|
}
|
|
state = ConnectionState.Disposed;
|
|
disposeEmitter.fire(undefined);
|
|
const error = new messages_1.ResponseError(messages_1.ErrorCodes.PendingResponseRejected, 'Pending response rejected since connection got disposed');
|
|
for (const promise of responsePromises.values()) {
|
|
promise.reject(error);
|
|
}
|
|
responsePromises = new Map();
|
|
requestTokens = new Map();
|
|
knownCanceledRequests = new Set();
|
|
messageQueue = new linkedMap_1.LinkedMap();
|
|
// Test for backwards compatibility
|
|
if (Is.func(messageWriter.dispose)) {
|
|
messageWriter.dispose();
|
|
}
|
|
if (Is.func(messageReader.dispose)) {
|
|
messageReader.dispose();
|
|
}
|
|
},
|
|
listen: () => {
|
|
throwIfClosedOrDisposed();
|
|
throwIfListening();
|
|
state = ConnectionState.Listening;
|
|
messageReader.listen(callback);
|
|
},
|
|
inspect: () => {
|
|
// eslint-disable-next-line no-console
|
|
(0, ral_1.default)().console.log('inspect');
|
|
}
|
|
};
|
|
connection.onNotification(LogTraceNotification.type, (params) => {
|
|
if (trace === Trace.Off || !tracer) {
|
|
return;
|
|
}
|
|
const verbose = trace === Trace.Verbose || trace === Trace.Compact;
|
|
tracer.log(params.message, verbose ? params.verbose : undefined);
|
|
});
|
|
connection.onNotification(ProgressNotification.type, (params) => {
|
|
const handler = progressHandlers.get(params.token);
|
|
if (handler) {
|
|
handler(params.value);
|
|
}
|
|
else {
|
|
unhandledProgressEmitter.fire(params);
|
|
}
|
|
});
|
|
return connection;
|
|
}
|
|
exports.createMessageConnection = createMessageConnection;
|