Enable per-request cancellation (#12371)
enable -per-request cancellation * restore request for deferred calls * add tests * introduce MultistepOperation * (test) subsequent request cancels the preceding one
This commit is contained in:
parent
1f484a9a03
commit
81f4e38643
|
@ -738,7 +738,7 @@ namespace Harness.LanguageService {
|
|||
// host to answer server queries about files on disk
|
||||
const serverHost = new SessionServerHost(clientHost);
|
||||
const server = new ts.server.Session(serverHost,
|
||||
{ isCancellationRequested: () => false },
|
||||
ts.server.nullCancellationToken,
|
||||
/*useOneInferredProject*/ false,
|
||||
/*typingsInstaller*/ undefined,
|
||||
Utils.byteLength,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace ts.projectSystem {
|
||||
import CommandNames = server.CommandNames;
|
||||
const nullCancellationToken = server.nullCancellationToken;
|
||||
|
||||
function createTestTypingsInstaller(host: server.ServerHost) {
|
||||
return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace ts.server {
|
|||
clearImmediate: noop,
|
||||
createHash: s => s
|
||||
};
|
||||
const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false };
|
||||
|
||||
const mockLogger: Logger = {
|
||||
close: noop,
|
||||
hasLevel(): boolean { return false; },
|
||||
|
|
|
@ -34,10 +34,6 @@ namespace ts.projectSystem {
|
|||
getLogFileName: (): string => undefined
|
||||
};
|
||||
|
||||
export const nullCancellationToken: HostCancellationToken = {
|
||||
isCancellationRequested: () => false
|
||||
};
|
||||
|
||||
export const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
|
||||
export const libFile: FileOrFolder = {
|
||||
path: "/a/lib/lib.d.ts",
|
||||
|
@ -158,17 +154,33 @@ namespace ts.projectSystem {
|
|||
}
|
||||
|
||||
class TestSession extends server.Session {
|
||||
private seq = 0;
|
||||
|
||||
getProjectService() {
|
||||
return this.projectService;
|
||||
}
|
||||
|
||||
public getSeq() {
|
||||
return this.seq;
|
||||
}
|
||||
|
||||
public getNextSeq() {
|
||||
return this.seq + 1;
|
||||
}
|
||||
|
||||
public executeCommandSeq<T extends server.protocol.Request>(request: Partial<T>) {
|
||||
this.seq++;
|
||||
request.seq = this.seq;
|
||||
request.type = "request";
|
||||
return this.executeCommand(<T>request);
|
||||
}
|
||||
};
|
||||
|
||||
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler) {
|
||||
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler, cancellationToken?: server.ServerCancellationToken) {
|
||||
if (typingsInstaller === undefined) {
|
||||
typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
|
||||
}
|
||||
|
||||
return new TestSession(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler);
|
||||
return new TestSession(host, cancellationToken || server.nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler);
|
||||
}
|
||||
|
||||
export interface CreateProjectServiceParameters {
|
||||
|
@ -191,7 +203,7 @@ namespace ts.projectSystem {
|
|||
}
|
||||
}
|
||||
export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}) {
|
||||
const cancellationToken = parameters.cancellationToken || nullCancellationToken;
|
||||
const cancellationToken = parameters.cancellationToken || server.nullCancellationToken;
|
||||
const logger = parameters.logger || nullLogger;
|
||||
const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false;
|
||||
return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler);
|
||||
|
@ -328,6 +340,8 @@ namespace ts.projectSystem {
|
|||
export class TestServerHost implements server.ServerHost {
|
||||
args: string[] = [];
|
||||
|
||||
private readonly output: string[] = [];
|
||||
|
||||
private fs: ts.FileMap<FSEntry>;
|
||||
private getCanonicalFileName: (s: string) => string;
|
||||
private toPath: (f: string) => Path;
|
||||
|
@ -477,6 +491,10 @@ namespace ts.projectSystem {
|
|||
this.timeoutCallbacks.invoke();
|
||||
}
|
||||
|
||||
runQueuedImmediateCallbacks() {
|
||||
this.immediateCallbacks.invoke();
|
||||
}
|
||||
|
||||
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
|
||||
return this.immediateCallbacks.register(callback, args);
|
||||
}
|
||||
|
@ -509,7 +527,17 @@ namespace ts.projectSystem {
|
|||
this.reloadFS(filesOrFolders);
|
||||
}
|
||||
|
||||
write() { }
|
||||
write(message: string) {
|
||||
this.output.push(message);
|
||||
}
|
||||
|
||||
getOutput(): ReadonlyArray<string> {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
clearOutput() {
|
||||
this.output.length = 0;
|
||||
}
|
||||
|
||||
readonly readFile = (s: string) => (<File>this.fs.get(this.toPath(s))).content;
|
||||
readonly resolvePath = (s: string) => s;
|
||||
|
@ -3131,6 +3159,200 @@ namespace ts.projectSystem {
|
|||
});
|
||||
});
|
||||
|
||||
describe("cancellationToken", () => {
|
||||
it("is attached to request", () => {
|
||||
const f1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: "let xyz = 1;"
|
||||
};
|
||||
const host = createServerHost([f1]);
|
||||
let expectedRequestId: number;
|
||||
const cancellationToken: server.ServerCancellationToken = {
|
||||
isCancellationRequested: () => false,
|
||||
setRequest: requestId => {
|
||||
if (expectedRequestId === undefined) {
|
||||
assert.isTrue(false, "unexpected call")
|
||||
}
|
||||
assert.equal(requestId, expectedRequestId);
|
||||
},
|
||||
resetRequest: noop
|
||||
}
|
||||
const session = createSession(host, /*typingsInstaller*/ undefined, /*projectServiceEventHandler*/ undefined, cancellationToken);
|
||||
|
||||
expectedRequestId = session.getNextSeq();
|
||||
session.executeCommandSeq(<server.protocol.OpenRequest>{
|
||||
command: "open",
|
||||
arguments: { file: f1.path }
|
||||
});
|
||||
|
||||
expectedRequestId = session.getNextSeq();
|
||||
session.executeCommandSeq(<server.protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
|
||||
expectedRequestId = session.getNextSeq();
|
||||
session.executeCommandSeq(<server.protocol.OccurrencesRequest>{
|
||||
command: "occurrences",
|
||||
arguments: { file: f1.path, line: 1, offset: 6 }
|
||||
});
|
||||
|
||||
expectedRequestId = 2;
|
||||
host.runQueuedImmediateCallbacks();
|
||||
expectedRequestId = 2;
|
||||
host.runQueuedImmediateCallbacks();
|
||||
});
|
||||
|
||||
it("Geterr is cancellable", () => {
|
||||
const f1 = {
|
||||
path: "/a/app.ts",
|
||||
content: "let x = 1"
|
||||
};
|
||||
const config = {
|
||||
path: "/a/tsconfig.json",
|
||||
content: JSON.stringify({
|
||||
compilerOptions: {}
|
||||
})
|
||||
};
|
||||
|
||||
let requestToCancel = -1;
|
||||
const cancellationToken: server.ServerCancellationToken = (function(){
|
||||
let currentId: number;
|
||||
return <server.ServerCancellationToken>{
|
||||
setRequest(requestId) {
|
||||
currentId = requestId;
|
||||
},
|
||||
resetRequest(requestId) {
|
||||
assert.equal(requestId, currentId, "unexpected request id in cancellation")
|
||||
currentId = undefined;
|
||||
},
|
||||
isCancellationRequested() {
|
||||
return requestToCancel === currentId;
|
||||
}
|
||||
}
|
||||
})();
|
||||
const host = createServerHost([f1, config]);
|
||||
const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken);
|
||||
{
|
||||
session.executeCommandSeq(<protocol.OpenRequest>{
|
||||
command: "open",
|
||||
arguments: { file: f1.path }
|
||||
});
|
||||
// send geterr for missing file
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: ["/a/missing"] }
|
||||
});
|
||||
// no files - expect 'completed' event
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 message");
|
||||
verifyRequestCompleted(session.getSeq(), 0);
|
||||
}
|
||||
{
|
||||
const getErrId = session.getNextSeq();
|
||||
// send geterr for a valid file
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
|
||||
assert.equal(host.getOutput().length, 0, "expect 0 messages");
|
||||
|
||||
// run new request
|
||||
session.executeCommandSeq(<protocol.ProjectInfoRequest>{
|
||||
command: "projectInfo",
|
||||
arguments: { file: f1.path }
|
||||
});
|
||||
host.clearOutput();
|
||||
|
||||
// cancel previously issued Geterr
|
||||
requestToCancel = getErrId;
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 message");
|
||||
verifyRequestCompleted(getErrId, 0);
|
||||
|
||||
requestToCancel = -1;
|
||||
}
|
||||
{
|
||||
const getErrId = session.getNextSeq();
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
assert.equal(host.getOutput().length, 0, "expect 0 messages");
|
||||
|
||||
// run first step
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 messages");
|
||||
const e1 = <protocol.Event>getMessage(0);
|
||||
assert.equal(e1.event, "syntaxDiag");
|
||||
host.clearOutput();
|
||||
|
||||
requestToCancel = getErrId;
|
||||
host.runQueuedImmediateCallbacks();
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 message");
|
||||
verifyRequestCompleted(getErrId, 0);
|
||||
|
||||
requestToCancel = -1;
|
||||
}
|
||||
{
|
||||
const getErrId = session.getNextSeq();
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
assert.equal(host.getOutput().length, 0, "expect 0 messages");
|
||||
|
||||
// run first step
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 messages");
|
||||
const e1 = <protocol.Event>getMessage(0);
|
||||
assert.equal(e1.event, "syntaxDiag");
|
||||
host.clearOutput();
|
||||
|
||||
host.runQueuedImmediateCallbacks();
|
||||
assert.equal(host.getOutput().length, 2, "expect 2 messages");
|
||||
const e2 = <protocol.Event>getMessage(0);
|
||||
assert.equal(e2.event, "semanticDiag");
|
||||
verifyRequestCompleted(getErrId, 1);
|
||||
|
||||
requestToCancel = -1;
|
||||
}
|
||||
{
|
||||
const getErr1 = session.getNextSeq();
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
assert.equal(host.getOutput().length, 0, "expect 0 messages");
|
||||
// run first step
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
assert.equal(host.getOutput().length, 1, "expect 1 messages");
|
||||
const e1 = <protocol.Event>getMessage(0);
|
||||
assert.equal(e1.event, "syntaxDiag");
|
||||
host.clearOutput();
|
||||
|
||||
session.executeCommandSeq(<protocol.GeterrRequest>{
|
||||
command: "geterr",
|
||||
arguments: { files: [f1.path] }
|
||||
});
|
||||
// make sure that getErr1 is completed
|
||||
verifyRequestCompleted(getErr1, 0);
|
||||
}
|
||||
|
||||
function verifyRequestCompleted(expectedSeq: number, n: number) {
|
||||
const event = <protocol.RequestCompletedEvent>getMessage(n);
|
||||
assert.equal(event.event, "requestCompleted");
|
||||
assert.equal(event.body.request_seq, expectedSeq, "expectedSeq");
|
||||
host.clearOutput();
|
||||
}
|
||||
|
||||
function getMessage(n: number) {
|
||||
return JSON.parse(server.extractMessage(host.getOutput()[n]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxNodeModuleJsDepth for inferred projects", () => {
|
||||
it("should be set to 2 if the project has js root files", () => {
|
||||
const file1: FileOrFolder = {
|
||||
|
@ -3184,5 +3406,4 @@ namespace ts.projectSystem {
|
|||
assert.isUndefined(project.getCompilerOptions().maxNodeModuleJsDepth);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
|
@ -1,14 +1,24 @@
|
|||
/// <reference types="node" />
|
||||
|
||||
|
||||
// TODO: extract services types
|
||||
interface HostCancellationToken {
|
||||
isCancellationRequested(): boolean;
|
||||
}
|
||||
/// <reference types="node"/>
|
||||
|
||||
import fs = require("fs");
|
||||
|
||||
function createCancellationToken(args: string[]): HostCancellationToken {
|
||||
interface ServerCancellationToken {
|
||||
isCancellationRequested(): boolean;
|
||||
setRequest(requestId: number): void;
|
||||
resetRequest(requestId: number): void;
|
||||
}
|
||||
|
||||
function pipeExists(name: string): boolean {
|
||||
try {
|
||||
fs.statSync(name);
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createCancellationToken(args: string[]): ServerCancellationToken {
|
||||
let cancellationPipeName: string;
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
if (args[i] === "--cancellationPipeName") {
|
||||
|
@ -17,18 +27,44 @@ function createCancellationToken(args: string[]): HostCancellationToken {
|
|||
}
|
||||
}
|
||||
if (!cancellationPipeName) {
|
||||
return { isCancellationRequested: () => false };
|
||||
return {
|
||||
isCancellationRequested: () => false,
|
||||
setRequest: (_requestId: number): void => void 0,
|
||||
resetRequest: (_requestId: number): void => void 0
|
||||
};
|
||||
}
|
||||
return {
|
||||
isCancellationRequested() {
|
||||
try {
|
||||
fs.statSync(cancellationPipeName);
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
// cancellationPipeName is a string without '*' inside that can optionally end with '*'
|
||||
// when client wants to signal cancellation it should create a named pipe with name=<cancellationPipeName>
|
||||
// server will synchronously check the presence of the pipe and treat its existance as indicator that current request should be canceled.
|
||||
// in case if client prefers to use more fine-grained schema than one name for all request it can add '*' to the end of cancelellationPipeName.
|
||||
// in this case pipe name will be build dynamically as <cancellationPipeName><request_seq>.
|
||||
if (cancellationPipeName.charAt(cancellationPipeName.length - 1) === "*") {
|
||||
const namePrefix = cancellationPipeName.slice(0, -1);
|
||||
if (namePrefix.length === 0 || namePrefix.indexOf("*") >= 0) {
|
||||
throw new Error("Invalid name for template cancellation pipe: it should have length greater than 2 characters and contain only one '*'.");
|
||||
}
|
||||
};
|
||||
let perRequestPipeName: string;
|
||||
let currentRequestId: number;
|
||||
return {
|
||||
isCancellationRequested: () => perRequestPipeName !== undefined && pipeExists(perRequestPipeName),
|
||||
setRequest(requestId: number) {
|
||||
currentRequestId = currentRequestId;
|
||||
perRequestPipeName = namePrefix + requestId;
|
||||
},
|
||||
resetRequest(requestId: number) {
|
||||
if (currentRequestId !== requestId) {
|
||||
throw new Error(`Mismatched request id, expected ${currentRequestId}, actual ${requestId}`);
|
||||
}
|
||||
perRequestPipeName = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
isCancellationRequested: () => pipeExists(cancellationPipeName),
|
||||
setRequest: (_requestId: number): void => void 0,
|
||||
resetRequest: (_requestId: number): void => void 0
|
||||
};
|
||||
}
|
||||
}
|
||||
export = createCancellationToken;
|
|
@ -13,6 +13,25 @@ namespace ts.server {
|
|||
findInComments: boolean;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function extractMessage(message: string) {
|
||||
// Read the content length
|
||||
const contentLengthPrefix = "Content-Length: ";
|
||||
const lines = message.split(/\r?\n/);
|
||||
Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response.");
|
||||
|
||||
const contentLengthText = lines[0];
|
||||
Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header.");
|
||||
const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length));
|
||||
|
||||
// Read the body
|
||||
const responseBody = lines[2];
|
||||
|
||||
// Verify content length
|
||||
Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length.");
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
export class SessionClient implements LanguageService {
|
||||
private sequence: number = 0;
|
||||
private lineMaps: ts.Map<number[]> = ts.createMap<number[]>();
|
||||
|
@ -84,7 +103,7 @@ namespace ts.server {
|
|||
while (!foundResponseMessage) {
|
||||
lastMessage = this.messages.shift();
|
||||
Debug.assert(!!lastMessage, "Did not receive any responses.");
|
||||
const responseBody = processMessage(lastMessage);
|
||||
const responseBody = extractMessage(lastMessage);
|
||||
try {
|
||||
response = JSON.parse(responseBody);
|
||||
// the server may emit events before emitting the response. We
|
||||
|
@ -109,24 +128,6 @@ namespace ts.server {
|
|||
Debug.assert(!!response.body, "Malformed response: Unexpected empty response body.");
|
||||
|
||||
return response;
|
||||
|
||||
function processMessage(message: string) {
|
||||
// Read the content length
|
||||
const contentLengthPrefix = "Content-Length: ";
|
||||
const lines = message.split("\r\n");
|
||||
Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response.");
|
||||
|
||||
const contentLengthText = lines[0];
|
||||
Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header.");
|
||||
const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length));
|
||||
|
||||
// Read the body
|
||||
const responseBody = lines[2];
|
||||
|
||||
// Verify content length
|
||||
Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length.");
|
||||
return responseBody;
|
||||
}
|
||||
}
|
||||
|
||||
openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void {
|
||||
|
|
|
@ -1766,6 +1766,20 @@ namespace ts.server.protocol {
|
|||
arguments: GeterrRequestArgs;
|
||||
}
|
||||
|
||||
export type RequestCompletedEventName = "requestCompleted";
|
||||
|
||||
/**
|
||||
* Event that is sent when server have finished processing request with specified id.
|
||||
*/
|
||||
export interface RequestCompletedEvent extends Event {
|
||||
event: RequestCompletedEventName;
|
||||
body: RequestCompletedEventBody;
|
||||
}
|
||||
|
||||
export interface RequestCompletedEventBody {
|
||||
request_seq: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item of diagnostic information found in a DiagnosticEvent message.
|
||||
*/
|
||||
|
|
|
@ -354,7 +354,7 @@ namespace ts.server {
|
|||
class IOSession extends Session {
|
||||
constructor(
|
||||
host: ServerHost,
|
||||
cancellationToken: HostCancellationToken,
|
||||
cancellationToken: ServerCancellationToken,
|
||||
installerEventPort: number,
|
||||
canUseEvents: boolean,
|
||||
useSingleInferredProject: boolean,
|
||||
|
@ -593,15 +593,13 @@ namespace ts.server {
|
|||
sys.gc = () => global.gc();
|
||||
}
|
||||
|
||||
let cancellationToken: HostCancellationToken;
|
||||
let cancellationToken: ServerCancellationToken;
|
||||
try {
|
||||
const factory = require("./cancellationToken");
|
||||
cancellationToken = factory(sys.args);
|
||||
}
|
||||
catch (e) {
|
||||
cancellationToken = {
|
||||
isCancellationRequested: () => false
|
||||
};
|
||||
cancellationToken = nullCancellationToken;
|
||||
};
|
||||
|
||||
let eventPort: number;
|
||||
|
|
|
@ -8,6 +8,17 @@ namespace ts.server {
|
|||
stack?: string;
|
||||
}
|
||||
|
||||
export interface ServerCancellationToken extends HostCancellationToken {
|
||||
setRequest(requestId: number): void;
|
||||
resetRequest(requestId: number): void;
|
||||
}
|
||||
|
||||
export const nullCancellationToken: ServerCancellationToken = {
|
||||
isCancellationRequested: () => false,
|
||||
setRequest: () => void 0,
|
||||
resetRequest: () => void 0
|
||||
};
|
||||
|
||||
function hrTimeToMilliseconds(time: number[]): number {
|
||||
const seconds = time[0];
|
||||
const nanoseconds = time[1];
|
||||
|
@ -193,18 +204,134 @@ namespace ts.server {
|
|||
return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to schedule next step in multistep operation
|
||||
*/
|
||||
interface NextStep {
|
||||
immediate(action: () => void): void;
|
||||
delay(ms: number, action: () => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* External capabilities used by multistep operation
|
||||
*/
|
||||
interface MultistepOperationHost {
|
||||
getCurrentRequestId(): number;
|
||||
sendRequestCompletedEvent(requestId: number): void;
|
||||
getServerHost(): ServerHost;
|
||||
isCancellationRequested(): boolean;
|
||||
executeWithRequestId(requestId: number, action: () => void): void;
|
||||
logError(error: Error, message: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents operation that can schedule its next step to be executed later.
|
||||
* Scheduling is done via instance of NextStep. If on current step subsequent step was not scheduled - operation is assumed to be completed.
|
||||
*/
|
||||
class MultistepOperation {
|
||||
private requestId: number;
|
||||
private timerHandle: any;
|
||||
private immediateId: any;
|
||||
private completed = true;
|
||||
private readonly next: NextStep;
|
||||
|
||||
constructor(private readonly operationHost: MultistepOperationHost) {
|
||||
this.next = {
|
||||
immediate: action => this.immediate(action),
|
||||
delay: (ms, action) => this.delay(ms, action)
|
||||
}
|
||||
}
|
||||
|
||||
public startNew(action: (next: NextStep) => void) {
|
||||
this.complete();
|
||||
this.requestId = this.operationHost.getCurrentRequestId();
|
||||
this.completed = false;
|
||||
this.executeAction(action);
|
||||
}
|
||||
|
||||
private complete() {
|
||||
if (!this.completed) {
|
||||
if (this.requestId) {
|
||||
this.operationHost.sendRequestCompletedEvent(this.requestId);
|
||||
}
|
||||
this.completed = true;
|
||||
}
|
||||
this.setTimerHandle(undefined);
|
||||
this.setImmediateId(undefined);
|
||||
}
|
||||
|
||||
private immediate(action: () => void) {
|
||||
const requestId = this.requestId;
|
||||
Debug.assert(requestId === this.operationHost.getCurrentRequestId(), "immediate: incorrect request id")
|
||||
this.setImmediateId(this.operationHost.getServerHost().setImmediate(() => {
|
||||
this.immediateId = undefined;
|
||||
this.operationHost.executeWithRequestId(requestId, () => this.executeAction(action));
|
||||
}));
|
||||
}
|
||||
|
||||
private delay(ms: number, action: () => void) {
|
||||
const requestId = this.requestId;
|
||||
Debug.assert(requestId === this.operationHost.getCurrentRequestId(), "delay: incorrect request id")
|
||||
this.setTimerHandle(this.operationHost.getServerHost().setTimeout(() => {
|
||||
this.timerHandle = undefined;
|
||||
this.operationHost.executeWithRequestId(requestId, () => this.executeAction(action));
|
||||
}, ms));
|
||||
}
|
||||
|
||||
private executeAction(action: (next: NextStep) => void) {
|
||||
let stop = false;
|
||||
try {
|
||||
if (this.operationHost.isCancellationRequested()) {
|
||||
stop = true;
|
||||
}
|
||||
else {
|
||||
action(this.next);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
stop = true;
|
||||
// ignore cancellation request
|
||||
if (!(e instanceof OperationCanceledException)) {
|
||||
this.operationHost.logError(e, `delayed processing of request ${this.requestId}`);
|
||||
}
|
||||
}
|
||||
if (stop || !this.hasPendingWork()) {
|
||||
this.complete();
|
||||
}
|
||||
}
|
||||
|
||||
private setTimerHandle(timerHandle: any) {;
|
||||
if (this.timerHandle !== undefined) {
|
||||
this.operationHost.getServerHost().clearTimeout(this.timerHandle);
|
||||
}
|
||||
this.timerHandle = timerHandle;
|
||||
}
|
||||
|
||||
private setImmediateId(immediateId: number) {
|
||||
if (this.immediateId !== undefined) {
|
||||
this.operationHost.getServerHost().clearImmediate(this.immediateId);
|
||||
}
|
||||
this.immediateId = immediateId;
|
||||
}
|
||||
|
||||
private hasPendingWork() {
|
||||
return !!this.timerHandle || !!this.immediateId;
|
||||
}
|
||||
}
|
||||
|
||||
export class Session implements EventSender {
|
||||
private readonly gcTimer: GcTimer;
|
||||
protected projectService: ProjectService;
|
||||
private errorTimer: any; /*NodeJS.Timer | number*/
|
||||
private immediateId: any;
|
||||
private changeSeq = 0;
|
||||
|
||||
private currentRequestId: number;
|
||||
private errorCheck: MultistepOperation;
|
||||
|
||||
private eventHander: ProjectServiceEventHandler;
|
||||
|
||||
constructor(
|
||||
private host: ServerHost,
|
||||
cancellationToken: HostCancellationToken,
|
||||
private readonly cancellationToken: ServerCancellationToken,
|
||||
useSingleInferredProject: boolean,
|
||||
protected readonly typingsInstaller: ITypingsInstaller,
|
||||
private byteLength: (buf: string, encoding?: string) => number,
|
||||
|
@ -217,17 +344,35 @@ namespace ts.server {
|
|||
? eventHandler || (event => this.defaultEventHandler(event))
|
||||
: undefined;
|
||||
|
||||
const multistepOperationHost: MultistepOperationHost = {
|
||||
executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action),
|
||||
getCurrentRequestId: () => this.currentRequestId,
|
||||
getServerHost: () => this.host,
|
||||
logError: (err, cmd) => this.logError(err, cmd),
|
||||
sendRequestCompletedEvent: requestId => this.sendRequestCompletedEvent(requestId),
|
||||
isCancellationRequested: () => cancellationToken.isCancellationRequested()
|
||||
}
|
||||
this.errorCheck = new MultistepOperation(multistepOperationHost);
|
||||
this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, this.eventHander);
|
||||
this.gcTimer = new GcTimer(host, /*delay*/ 7000, logger);
|
||||
}
|
||||
|
||||
private sendRequestCompletedEvent(requestId: number): void {
|
||||
const event: protocol.RequestCompletedEvent = {
|
||||
seq: 0,
|
||||
type: "event",
|
||||
event: "requestCompleted",
|
||||
body: { request_seq: requestId }
|
||||
};
|
||||
this.send(event);
|
||||
}
|
||||
|
||||
private defaultEventHandler(event: ProjectServiceEvent) {
|
||||
switch (event.eventName) {
|
||||
case ContextEvent:
|
||||
const { project, fileName } = event.data;
|
||||
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
|
||||
this.updateErrorCheck([{ fileName, project }], this.changeSeq,
|
||||
(n) => n === this.changeSeq, 100);
|
||||
this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], this.changeSeq, (n) => n === this.changeSeq, 100));
|
||||
break;
|
||||
case ConfigFileDiagEvent:
|
||||
const { triggerFile, configFileName, diagnostics } = event.data;
|
||||
|
@ -284,7 +429,7 @@ namespace ts.server {
|
|||
seq: 0,
|
||||
type: "event",
|
||||
event: eventName,
|
||||
body: info,
|
||||
body: info
|
||||
};
|
||||
this.send(ev);
|
||||
}
|
||||
|
@ -342,18 +487,11 @@ namespace ts.server {
|
|||
}, ms);
|
||||
}
|
||||
|
||||
private updateErrorCheck(checkList: PendingErrorCheck[], seq: number,
|
||||
matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200, requireOpen = true) {
|
||||
private updateErrorCheck(next: NextStep, checkList: PendingErrorCheck[], seq: number, matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200, requireOpen = true) {
|
||||
if (followMs > ms) {
|
||||
followMs = ms;
|
||||
}
|
||||
if (this.errorTimer) {
|
||||
this.host.clearTimeout(this.errorTimer);
|
||||
}
|
||||
if (this.immediateId) {
|
||||
this.host.clearImmediate(this.immediateId);
|
||||
this.immediateId = undefined;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
const checkOne = () => {
|
||||
if (matchSeq(seq)) {
|
||||
|
@ -361,21 +499,18 @@ namespace ts.server {
|
|||
index++;
|
||||
if (checkSpec.project.containsFile(checkSpec.fileName, requireOpen)) {
|
||||
this.syntacticCheck(checkSpec.fileName, checkSpec.project);
|
||||
this.immediateId = this.host.setImmediate(() => {
|
||||
next.immediate(() => {
|
||||
this.semanticCheck(checkSpec.fileName, checkSpec.project);
|
||||
this.immediateId = undefined;
|
||||
if (checkList.length > index) {
|
||||
this.errorTimer = this.host.setTimeout(checkOne, followMs);
|
||||
}
|
||||
else {
|
||||
this.errorTimer = undefined;
|
||||
next.delay(followMs, checkOne);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ((checkList.length > index) && (matchSeq(seq))) {
|
||||
this.errorTimer = this.host.setTimeout(checkOne, ms);
|
||||
next.delay(ms, checkOne);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1087,7 +1222,7 @@ namespace ts.server {
|
|||
}
|
||||
}
|
||||
|
||||
private getDiagnostics(delay: number, fileNames: string[]) {
|
||||
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
|
||||
const checkList = fileNames.reduce((accum: PendingErrorCheck[], uncheckedFileName: string) => {
|
||||
const fileName = toNormalizedPath(uncheckedFileName);
|
||||
const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
|
||||
|
@ -1098,7 +1233,7 @@ namespace ts.server {
|
|||
}, []);
|
||||
|
||||
if (checkList.length > 0) {
|
||||
this.updateErrorCheck(checkList, this.changeSeq, (n) => n === this.changeSeq, delay);
|
||||
this.updateErrorCheck(next, checkList, this.changeSeq, (n) => n === this.changeSeq, delay);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1335,7 +1470,7 @@ namespace ts.server {
|
|||
: spans;
|
||||
}
|
||||
|
||||
getDiagnosticsForProject(delay: number, fileName: string) {
|
||||
private getDiagnosticsForProject(next: NextStep, delay: number, fileName: string): void {
|
||||
const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true);
|
||||
if (languageServiceDisabled) {
|
||||
return;
|
||||
|
@ -1373,7 +1508,7 @@ namespace ts.server {
|
|||
const checkList = fileNamesInProject.map(fileName => ({ fileName, project }));
|
||||
// Project level error analysis runs on background files too, therefore
|
||||
// doesn't require the file to be opened
|
||||
this.updateErrorCheck(checkList, this.changeSeq, (n) => n == this.changeSeq, delay, 200, /*requireOpen*/ false);
|
||||
this.updateErrorCheck(next, checkList, this.changeSeq, (n) => n == this.changeSeq, delay, 200, /*requireOpen*/ false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1550,13 +1685,13 @@ namespace ts.server {
|
|||
[CommandNames.SyntacticDiagnosticsSync]: (request: protocol.SyntacticDiagnosticsSyncRequest) => {
|
||||
return this.requiredResponse(this.getSyntacticDiagnosticsSync(request.arguments));
|
||||
},
|
||||
[CommandNames.Geterr]: (request: protocol.Request) => {
|
||||
const geterrArgs = <protocol.GeterrRequestArgs>request.arguments;
|
||||
return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false };
|
||||
[CommandNames.Geterr]: (request: protocol.GeterrRequest) => {
|
||||
this.errorCheck.startNew(next => this.getDiagnostics(next, request.arguments.delay, request.arguments.files));
|
||||
return this.notRequired();
|
||||
},
|
||||
[CommandNames.GeterrForProject]: (request: protocol.Request) => {
|
||||
const { file, delay } = <protocol.GeterrForProjectRequestArgs>request.arguments;
|
||||
return { response: this.getDiagnosticsForProject(delay, file), responseRequired: false };
|
||||
[CommandNames.GeterrForProject]: (request: protocol.GeterrForProjectRequest) => {
|
||||
this.errorCheck.startNew(next => this.getDiagnosticsForProject(next, request.arguments.delay, request.arguments.file));
|
||||
return this.notRequired();
|
||||
},
|
||||
[CommandNames.Change]: (request: protocol.ChangeRequest) => {
|
||||
this.change(request.arguments);
|
||||
|
@ -1643,10 +1778,32 @@ namespace ts.server {
|
|||
this.handlers.set(command, handler);
|
||||
}
|
||||
|
||||
private setCurrentRequest(requestId: number): void {
|
||||
Debug.assert(this.currentRequestId === undefined);
|
||||
this.currentRequestId = requestId;
|
||||
this.cancellationToken.setRequest(requestId);
|
||||
}
|
||||
|
||||
private resetCurrentRequest(requestId: number): void {
|
||||
Debug.assert(this.currentRequestId === requestId);
|
||||
this.currentRequestId = undefined;
|
||||
this.cancellationToken.resetRequest(requestId);
|
||||
}
|
||||
|
||||
public executeWithRequestId<T>(requestId: number, f: () => T) {
|
||||
try {
|
||||
this.setCurrentRequest(requestId);
|
||||
return f();
|
||||
}
|
||||
finally {
|
||||
this.resetCurrentRequest(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
public executeCommand(request: protocol.Request): { response?: any, responseRequired?: boolean } {
|
||||
const handler = this.handlers.get(request.command);
|
||||
if (handler) {
|
||||
return handler(request);
|
||||
return this.executeWithRequestId(request.seq, () => handler(request));
|
||||
}
|
||||
else {
|
||||
this.logger.msg(`Unrecognized JSON command: ${JSON.stringify(request)}`, Msg.Err);
|
||||
|
|
Loading…
Reference in a new issue