From 23da1cf822c6965fdb34ae3df8a72ca174ea3137 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 18:28:50 -0800 Subject: [PATCH 01/21] send all events through common stream --- src/server/editorServices.ts | 4 +- src/server/server.ts | 66 ++++++++--------------- src/server/session.ts | 100 ++++++++++++++++++++++++++--------- src/server/typingsCache.ts | 2 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2205f65cbe..d7ee0af7ef 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,6 +16,7 @@ namespace ts.server { export const ProjectInfoTelemetryEvent = "projectInfo"; // tslint:enable variable-name + // TODO: make these inherit from protocol.Event? export interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { openFiles: string[]; }; @@ -320,6 +321,7 @@ namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + eventSender?: EventSender; } type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; @@ -436,7 +438,7 @@ namespace ts.server { this.loadTypesMap(); } - this.typingsInstaller.attach(this); + this.typingsInstaller.attach(this, opts.eventSender); this.typingsCache = new TypingsCache(this.typingsInstaller); diff --git a/src/server/server.ts b/src/server/server.ts index f4faa0d3c7..97eba99188 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,3 @@ -/// /// /// @@ -7,7 +6,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - installerEventPort: number; + eventPort: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; @@ -22,10 +21,6 @@ namespace ts.server { allowLocalPluginLoads: boolean; } - const net: { - connect(options: { port: number }, onConnect?: () => void): NodeSocket - } = require("net"); - const childProcess: { fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike }): NodeChildProcess; execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike }): string | Buffer; @@ -83,10 +78,6 @@ namespace ts.server { pid: number; } - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - interface ReadLineOptions { input: NodeJS.ReadableStream; output?: NodeJS.WritableStream; @@ -244,9 +235,8 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; private installerPidReported = false; - private socket: NodeSocket; private projectService: ProjectService; - private eventSender: EventSender; + private eventSender: EventSender | undefined; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -267,18 +257,10 @@ namespace ts.server { private readonly telemetryEnabled: boolean, private readonly logger: server.Logger, private readonly host: ServerHost, - eventPort: number, readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, - private readonly npmLocation: string | undefined, - private newLine: string) { - if (eventPort) { - const s = net.connect({ port: eventPort }, () => { - this.socket = s; - this.reportInstallerProcessId(); - }); - } + private readonly npmLocation: string | undefined) { } isKnownTypesPackageName(name: string): boolean { @@ -310,26 +292,23 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.socket && this.installer) { - this.sendEvent(0, "typingsInstallerPid", { pid: this.installer.pid }); + if (this.installer && this.eventSender) { + this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } } - private sendEvent(seq: number, event: string, body: any): void { - this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8"); - } - setTelemetrySender(telemetrySender: EventSender) { - this.eventSender = telemetrySender; - } - - attach(projectService: ProjectService) { + attach(projectService: ProjectService, eventSender?: EventSender) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } + if (eventSender) { + this.eventSender = eventSender; + } + const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; if (this.telemetryEnabled) { args.push(Arguments.EnableTelemetry); @@ -353,10 +332,10 @@ namespace ts.server { if (match) { // if port is specified - use port + 1 // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1 - const currentPort = match[2] !== undefined - ? +match[2] - : match[1].charAt(0) === "d" ? 5858 : 9229; - execArgv.push(`--${match[1]}=${currentPort + 1}`); + // const currentPort = match[2] !== undefined + // ? +match[2] + // : match[1].charAt(0) === "d" ? 5858 : 9229; + // execArgv.push(`--${match[1]}=${currentPort + 1}`); break; } } @@ -508,8 +487,8 @@ namespace ts.server { this.projectService.updateTypingsForProject(response); - if (this.socket) { - this.sendEvent(0, "setTypings", response); + if (this.eventSender) { + this.eventSender.event(response, "setTypings"); } break; @@ -530,10 +509,10 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { - const { host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, host.newLine); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); super({ host, @@ -545,13 +524,10 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, + eventPort, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); - - if (telemetryEnabled && typingsInstaller) { - typingsInstaller.setTelemetrySender(this); - } } exit() { @@ -936,8 +912,8 @@ namespace ts.server { const options: IoSessionOptions = { host: sys, cancellationToken, - installerEventPort: eventPort, - canUseEvents: eventPort === undefined, + eventPort, + canUseEvents: true, useSingleInferredProject, useInferredProjectPerProjectRoot, disableAutomaticTypingAcquisition, diff --git a/src/server/session.ts b/src/server/session.ts index 6c97c3c8bd..b728ea6d55 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,9 +1,19 @@ +/// /// /// /// /// namespace ts.server { + + interface NodeSocket { + write(data: string, encoding: string): boolean; + } + + const net: { + connect(options: { port: number }, onConnect?: () => void): NodeSocket + } = require("net"); + interface StackTraceError extends Error { stack?: string; } @@ -253,6 +263,10 @@ namespace ts.server { hrtime: (start?: number[]) => number[]; logger: Logger; canUseEvents: boolean; + /** + * If defined, the Session will send events through `eventPort` instead of stdout. + */ + eventPort?: number; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -269,15 +283,19 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private eventHandler: ProjectServiceEventHandler; - private host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; private byteLength: (buf: string, encoding?: string) => number; private hrtime: (start?: number[]) => number[]; protected logger: Logger; + private canUseEvents: boolean; + private eventPort: number | undefined; + private eventSocket: NodeSocket; + private eventHandler: ProjectServiceEventHandler; + public readonly event: EventSender["event"]; + private socketEventQueue: { info: any, eventName: string}[] | undefined; constructor(opts: SessionOptions) { this.host = opts.host; @@ -286,14 +304,49 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; + this.eventPort = opts.eventPort; this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; + if (!this.canUseEvents) { + this.event = noop; + } + else if (this.eventPort) { + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + this.clearSocketEventQueue(); + }); + + this.event = function (info: T, eventName: string) { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ info, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(info, eventName); + } + }; + } + else { + this.event = function (info: T, eventName: string) { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body: info + }; + this.send(ev); + }; + } + this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; - const multistepOperationHost: MultistepOperationHost = { executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action), getCurrentRequestId: () => this.currentRequestId, @@ -314,20 +367,26 @@ namespace ts.server { eventHandler: this.eventHandler, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, - allowLocalPluginLoads: opts.allowLocalPluginLoads + allowLocalPluginLoads: opts.allowLocalPluginLoads, + eventSender: this }; this.projectService = new ProjectService(settings); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } + private clearSocketEventQueue() { + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.info, event.eventName); + } + this.socketEventQueue = undefined; + } + + private writeToEventSocket(info: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + private sendRequestCompletedEvent(requestId: number): void { - const event: protocol.RequestCompletedEvent = { - seq: 0, - type: "event", - event: "requestCompleted", - body: { request_seq: requestId } - }; - this.send(event); + this.event({ request_seq: requestId }, "requestCompleted"); } private defaultEventHandler(event: ProjectServiceEvent) { @@ -392,26 +451,15 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event" && !this.canUseEvents) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; + if (msg.type === "event") { + Debug.assert(this.canUseEvents); + Debug.assert(!this.eventPort); } this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } - public event(info: T, eventName: string) { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body: info - }; - this.send(ev); - } - // For backwards-compatibility only. + /** @deprecated */ public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg); } diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index cde303bfd3..6fbf093943 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -10,7 +10,7 @@ namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService): void; + attach(projectService: ProjectService, eventSender?: EventSender): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } From 0d3002c5f0ae1d3a34e238dfda5fc1a9c24919d9 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 22:17:05 -0800 Subject: [PATCH 02/21] accept baselines --- .../reference/api/tsserverlibrary.d.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a3499de08a..59e289ed4a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6918,6 +6918,10 @@ declare namespace ts.server { hrtime: (start?: number[]) => number[]; logger: Logger; canUseEvents: boolean; + /** + * If defined, the Session will send events through `eventPort` instead of stdout. + */ + eventPort?: number; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6930,7 +6934,6 @@ declare namespace ts.server { private changeSeq; private currentRequestId; private errorCheck; - private eventHandler; private host; private readonly cancellationToken; protected readonly typingsInstaller: ITypingsInstaller; @@ -6938,13 +6941,20 @@ declare namespace ts.server { private hrtime; protected logger: Logger; private canUseEvents; + private eventPort; + private eventSocket; + private eventHandler; + readonly event: EventSender["event"]; + private socketEventQueue; constructor(opts: SessionOptions); + private clearSocketEventQueue(); + private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); private projectsUpdatedInBackgroundEvent(openFiles); logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; - event(info: T, eventName: string): void; + /** @deprecated */ output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; private doOutput(info, cmdName, reqSeq, success, message?); private semanticCheck(file, project); @@ -7102,7 +7112,7 @@ declare namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService): void; + attach(projectService: ProjectService, eventSender?: EventSender): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } @@ -7461,6 +7471,7 @@ declare namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + eventSender?: EventSender; } class ProjectService { readonly typingsCache: TypingsCache; From 9f991d375a95c399f5d0f672b0a382851de46b50 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 23:03:16 -0800 Subject: [PATCH 03/21] cleanup --- src/server/editorServices.ts | 1 - src/server/server.ts | 10 ++++---- src/server/session.ts | 23 ++++++++++--------- .../reference/api/tsserverlibrary.d.ts | 1 - 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d7ee0af7ef..07726aeba2 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,7 +16,6 @@ namespace ts.server { export const ProjectInfoTelemetryEvent = "projectInfo"; // tslint:enable variable-name - // TODO: make these inherit from protocol.Event? export interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { openFiles: string[]; }; diff --git a/src/server/server.ts b/src/server/server.ts index 97eba99188..b9dde6c1ed 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -292,7 +292,7 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.installer && this.eventSender) { + if (this.eventSender && this.installer) { this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } @@ -332,10 +332,10 @@ namespace ts.server { if (match) { // if port is specified - use port + 1 // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1 - // const currentPort = match[2] !== undefined - // ? +match[2] - // : match[1].charAt(0) === "d" ? 5858 : 9229; - // execArgv.push(`--${match[1]}=${currentPort + 1}`); + const currentPort = match[2] !== undefined + ? +match[2] + : match[1].charAt(0) === "d" ? 5858 : 9229; + execArgv.push(`--${match[1]}=${currentPort + 1}`); break; } } diff --git a/src/server/session.ts b/src/server/session.ts index b728ea6d55..5fe38fe64c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -315,7 +315,13 @@ namespace ts.server { else if (this.eventPort) { const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; - this.clearSocketEventQueue(); + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.info, event.eventName); + } + this.socketEventQueue = undefined; + } }); this.event = function (info: T, eventName: string) { @@ -374,13 +380,6 @@ namespace ts.server { this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } - private clearSocketEventQueue() { - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.info, event.eventName); - } - this.socketEventQueue = undefined; - } - private writeToEventSocket(info: any, eventName: string): void { this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); } @@ -451,9 +450,11 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event") { - Debug.assert(this.canUseEvents); - Debug.assert(!this.eventPort); + if (msg.type === "event" && !this.canUseEvents) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; } this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 59e289ed4a..5c1d6401c1 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6947,7 +6947,6 @@ declare namespace ts.server { readonly event: EventSender["event"]; private socketEventQueue; constructor(opts: SessionOptions); - private clearSocketEventQueue(); private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); From 1746f4b3d36a5ebbfce5bb7c5f6963eeec32fca8 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 15:10:44 -0800 Subject: [PATCH 04/21] factor out socket-event-sending --- src/server/editorServices.ts | 4 +- src/server/server.ts | 4 +- src/server/session.ts | 105 +++++++++++++++++++++-------------- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 07726aeba2..610903135c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1441,7 +1441,9 @@ namespace ts.server { } this.seenProjects.set(projectKey, true); - if (!this.eventHandler) return; + if (!this.eventHandler) { + return; + } const data: ProjectInfoTelemetryEventData = { projectId: this.host.createHash(projectKey), diff --git a/src/server/server.ts b/src/server/server.ts index b9dde6c1ed..8acc42c76c 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -6,7 +6,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - eventPort: number; + eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; @@ -872,7 +872,7 @@ namespace ts.server { cancellationToken = nullCancellationToken; } - let eventPort: number; + let eventPort: number | undefined; { const str = findArgument("--eventPort"); const v = str && parseInt(str); diff --git a/src/server/session.ts b/src/server/session.ts index 5fe38fe64c..13f65a8cb4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -115,10 +115,6 @@ namespace ts.server { project: Project; } - export interface EventSender { - event(payload: T, eventName: string): void; - } - function allEditsBeforePos(edits: ts.TextChange[], pos: number) { for (const edit of edits) { if (textSpanEnd(edit.span) >= pos) { @@ -253,6 +249,55 @@ namespace ts.server { } } + export type Event = (body: T, eventName: string) => void; + + export interface EventSender { + event: Event; + } + + class SocketEventSender implements EventSender { + private host: ServerHost; + private logger: Logger; + private eventPort: number; + private eventSocket: NodeSocket; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + + constructor(host: ServerHost, logger: Logger, eventPort: number) { + this.host = host; + this.logger = logger; + this.eventPort = eventPort; + + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + public event(body: T, eventName: string): void { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -262,9 +307,13 @@ namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; /** - * If defined, the Session will send events through `eventPort` instead of stdout. + * If defined, specifies the socket to send events to the client. + * Otherwise, events are sent through the host. */ eventPort?: number; eventHandler?: ProjectServiceEventHandler; @@ -276,6 +325,8 @@ namespace ts.server { } export class Session implements EventSender { + public readonly event: Event; + private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -292,10 +343,7 @@ namespace ts.server { private canUseEvents: boolean; private eventPort: number | undefined; - private eventSocket: NodeSocket; private eventHandler: ProjectServiceEventHandler; - public readonly event: EventSender["event"]; - private socketEventQueue: { info: any, eventName: string}[] | undefined; constructor(opts: SessionOptions) { this.host = opts.host; @@ -308,43 +356,18 @@ namespace ts.server { this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; - - if (!this.canUseEvents) { - this.event = noop; - } - else if (this.eventPort) { - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.info, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - - this.event = function (info: T, eventName: string) { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ info, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(info, eventName); - } - }; + + if (this.eventPort && this.canUseEvents) { + const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); + this.event = eventSender.event; } else { - this.event = function (info: T, eventName: string) { + this.event = function (body: T, eventName: string): void { const ev: protocol.Event = { seq: 0, type: "event", event: eventName, - body: info + body }; this.send(ev); }; @@ -380,10 +403,6 @@ namespace ts.server { this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } - private writeToEventSocket(info: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); - } - private sendRequestCompletedEvent(requestId: number): void { this.event({ request_seq: requestId }, "requestCompleted"); } From c453e081354f7b3449a487cfec2654b7ef5339c9 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 15:10:58 -0800 Subject: [PATCH 05/21] accept baseline --- .../reference/api/tsserverlibrary.d.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5c1d6401c1..467bce5db6 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6902,12 +6902,13 @@ declare namespace ts.server { fileName: NormalizedPath; project: Project; } - interface EventSender { - event(payload: T, eventName: string): void; - } type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; + type Event = (body: T, eventName: string) => void; + interface EventSender { + event: Event; + } interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -6917,9 +6918,13 @@ declare namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; /** - * If defined, the Session will send events through `eventPort` instead of stdout. + * If defined, specifies the socket to send events to the client. + * Otherwise, events are sent through the host. */ eventPort?: number; eventHandler?: ProjectServiceEventHandler; @@ -6929,6 +6934,7 @@ declare namespace ts.server { allowLocalPluginLoads?: boolean; } class Session implements EventSender { + readonly event: Event; private readonly gcTimer; protected projectService: ProjectService; private changeSeq; @@ -6942,12 +6948,8 @@ declare namespace ts.server { protected logger: Logger; private canUseEvents; private eventPort; - private eventSocket; private eventHandler; - readonly event: EventSender["event"]; - private socketEventQueue; constructor(opts: SessionOptions); - private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); private projectsUpdatedInBackgroundEvent(openFiles); From bf8f9be6775234ba6405236cb9319e0bd9cb87f0 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 16:42:18 -0800 Subject: [PATCH 06/21] expose event to overwrite in SessionOptions * need to retain eventPort and canUseEvents arguments and functionality for backwards compatibility. --- src/server/server.ts | 8 ++++++++ src/server/session.ts | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 8acc42c76c..22112c4cb7 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -510,6 +510,13 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + + let event: Event; + if (canUseEvents && eventPort) { + const eventSender = new SocketEventSender(host, logger, eventPort); + event = eventSender.event; + } + const typingsInstaller = disableAutomaticTypingAcquisition ? undefined : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); @@ -525,6 +532,7 @@ namespace ts.server { logger, canUseEvents, eventPort, + event, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index 13f65a8cb4..8ad9f7b245 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -255,7 +255,8 @@ namespace ts.server { event: Event; } - class SocketEventSender implements EventSender { + /** @internal */ + export class SocketEventSender implements EventSender { private host: ServerHost; private logger: Logger; private eventPort: number; @@ -316,6 +317,11 @@ namespace ts.server { * Otherwise, events are sent through the host. */ eventPort?: number; + /** + * An optional callback overriding the default behavior for sending events. + * if set, `canUseEvents` and `eventPort` are ignored. + */ + event?: Event; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -353,11 +359,14 @@ namespace ts.server { this.hrtime = opts.hrtime; this.logger = opts.logger; this.eventPort = opts.eventPort; - this.canUseEvents = opts.canUseEvents; + this.canUseEvents = opts.canUseEvents || !!opts.event; const { throttleWaitMilliseconds } = opts; - - if (this.eventPort && this.canUseEvents) { + + if (opts.event) { + this.event = opts.event; + } + else if (this.eventPort && this.canUseEvents) { const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); this.event = eventSender.event; } From ab332cffac8aeaf0e06dca7a112575d975ab613f Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 16:44:04 -0800 Subject: [PATCH 07/21] update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 467bce5db6..1dca7c720b 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6927,6 +6927,11 @@ declare namespace ts.server { * Otherwise, events are sent through the host. */ eventPort?: number; + /** + * An optional callback overriding the default behavior for sending events. + * if set, `canUseEvents` and `eventPort` are ignored. + */ + event?: Event; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; From 6a910919acd9731316b4ddae6cbff6d8ee16f4fd Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 18:09:32 -0800 Subject: [PATCH 08/21] use arrow to capture this --- src/server/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 9664b830bc..65d113327b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -260,7 +260,7 @@ namespace ts.server { private host: ServerHost; private logger: Logger; private eventPort: number; - private eventSocket: NodeSocket; + private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; constructor(host: ServerHost, logger: Logger, eventPort: number) { @@ -280,7 +280,7 @@ namespace ts.server { }); } - public event(body: T, eventName: string): void { + public event = (body: T, eventName: string) => { if (!this.eventSocket) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); From 930be75e0ca4f19a4ff4988f0c8cd4c22183e536 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 18:14:15 -0800 Subject: [PATCH 09/21] lint --- src/server/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 22112c4cb7..2fa84e04dc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -510,13 +510,13 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - + let event: Event; if (canUseEvents && eventPort) { const eventSender = new SocketEventSender(host, logger, eventPort); event = eventSender.event; } - + const typingsInstaller = disableAutomaticTypingAcquisition ? undefined : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); From 93ff7ba2c5b4234be1c02f71beb89880283ea487 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 22:27:01 -0800 Subject: [PATCH 10/21] lint more --- src/server/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/session.ts b/src/server/session.ts index 65d113327b..5912536f4f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -295,7 +295,7 @@ namespace ts.server { } private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); } } From 005c86340f02fe904a410ef10ff71db4e45c5841 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 13:12:48 -0800 Subject: [PATCH 11/21] move socketSender to IOSession --- src/server/server.ts | 56 ++++++++++++++++++++++++++++++++++++- src/server/session.ts | 65 ------------------------------------------- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 2fa84e04dc..68909227ac 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -6,6 +6,10 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; + /** + * If defined, specifies the socket used to send events to the client. + * Otherwise, events are sent through the host. + */ eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; @@ -31,6 +35,14 @@ namespace ts.server { tmpdir(): string; } = require("os"); + interface NodeSocket { + write(data: string, encoding: string): boolean; + } + + const net: { + connect(options: { port: number }, onConnect?: () => void): NodeSocket + } = require("net"); + function getGlobalTypingsCacheLocation() { switch (process.platform) { case "win32": { @@ -507,6 +519,49 @@ namespace ts.server { } } + class SocketEventSender implements EventSender { + private host: ServerHost; + private logger: Logger; + private eventPort: number; + private eventSocket: NodeSocket | undefined; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + + constructor(host: ServerHost, logger: Logger, eventPort: number) { + this.host = host; + this.logger = logger; + this.eventPort = eventPort; + + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + public event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + } + class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; @@ -531,7 +586,6 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - eventPort, event, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, diff --git a/src/server/session.ts b/src/server/session.ts index 5912536f4f..73575672be 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,19 +1,9 @@ -/// /// /// /// /// namespace ts.server { - - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - - const net: { - connect(options: { port: number }, onConnect?: () => void): NodeSocket - } = require("net"); - interface StackTraceError extends Error { stack?: string; } @@ -255,50 +245,6 @@ namespace ts.server { event: Event; } - /** @internal */ - export class SocketEventSender implements EventSender { - private host: ServerHost; - private logger: Logger; - private eventPort: number; - private eventSocket: NodeSocket | undefined; - private socketEventQueue: { body: any, eventName: string }[] | undefined; - - constructor(host: ServerHost, logger: Logger, eventPort: number) { - this.host = host; - this.logger = logger; - this.eventPort = eventPort; - - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.body, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - } - - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - } - - private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); - } - } - export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -312,11 +258,6 @@ namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * If defined, specifies the socket to send events to the client. - * Otherwise, events are sent through the host. - */ - eventPort?: number; /** * An optional callback overriding the default behavior for sending events. * if set, `canUseEvents` and `eventPort` are ignored. @@ -348,7 +289,6 @@ namespace ts.server { protected logger: Logger; private canUseEvents: boolean; - private eventPort: number | undefined; private eventHandler: ProjectServiceEventHandler; constructor(opts: SessionOptions) { @@ -358,7 +298,6 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; - this.eventPort = opts.eventPort; this.canUseEvents = opts.canUseEvents || !!opts.event; const { throttleWaitMilliseconds } = opts; @@ -366,10 +305,6 @@ namespace ts.server { if (opts.event) { this.event = opts.event; } - else if (this.eventPort && this.canUseEvents) { - const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); - this.event = eventSender.event; - } else { this.event = function (body: T, eventName: string): void { const ev: protocol.Event = { From d6c3a15ea62c72b54168ca6d64f93eff4be36aa7 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 18:51:25 -0800 Subject: [PATCH 12/21] Move event into IoSession --- src/server/editorServices.ts | 3 +- src/server/server.ts | 67 ++++++++++++------------- src/server/session.ts | 94 +++++++++++++++++++++++++----------- src/server/typingsCache.ts | 2 +- 4 files changed, 97 insertions(+), 69 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 18231e0632..093437e556 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -320,7 +320,6 @@ namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; - eventSender?: EventSender; } type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; @@ -441,7 +440,7 @@ namespace ts.server { this.logger.info("No types map provided; using the default"); } - this.typingsInstaller.attach(this, opts.eventSender); + this.typingsInstaller.attach(this); this.typingsCache = new TypingsCache(this.typingsInstaller); diff --git a/src/server/server.ts b/src/server/server.ts index 68909227ac..2f5061bc71 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,9 +7,9 @@ namespace ts.server { cancellationToken: ServerCancellationToken; canUseEvents: boolean; /** - * If defined, specifies the socket used to send events to the client. - * Otherwise, events are sent through the host. - */ + * If defined, specifies the socket used to send events to the client. + * Otherwise, events are sent through the host. + */ eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; @@ -248,7 +248,6 @@ namespace ts.server { private installer: NodeChildProcess; private installerPidReported = false; private projectService: ProjectService; - private eventSender: EventSender | undefined; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -272,7 +271,11 @@ namespace ts.server { readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, - private readonly npmLocation: string | undefined) { + private readonly npmLocation: string | undefined, + /** + * If undefined, event-related work will be suppressed. + */ + private eventSender: EventSender | undefined) { } isKnownTypesPackageName(name: string): boolean { @@ -311,16 +314,12 @@ namespace ts.server { } - attach(projectService: ProjectService, eventSender?: EventSender) { + attach(projectService: ProjectService) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } - if (eventSender) { - this.eventSender = eventSender; - } - const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; if (this.telemetryEnabled) { args.push(Arguments.EnableTelemetry); @@ -519,17 +518,15 @@ namespace ts.server { } } - class SocketEventSender implements EventSender { - private host: ServerHost; - private logger: Logger; - private eventPort: number; + class SocketEventSender extends DefaultMessageSender { private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; - constructor(host: ServerHost, logger: Logger, eventPort: number) { - this.host = host; - this.logger = logger; - this.eventPort = eventPort; + constructor(host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + private eventPort: number) { + super(host, byteLength, logger, /*canUseEvents*/ true); const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; @@ -541,20 +538,20 @@ namespace ts.server { this.socketEventQueue = undefined; } }); - } - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + this.event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + }; } private writeToEventSocket(body: any, eventName: string): void { @@ -566,15 +563,11 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - let event: Event; - if (canUseEvents && eventPort) { - const eventSender = new SocketEventSender(host, logger, eventPort); - event = eventSender.event; - } + const messageSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultMessageSender(host, Buffer.byteLength, logger, canUseEvents); const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? messageSender : undefined); super({ host, @@ -586,7 +579,7 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - event, + messageSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index 73575672be..ff41138ae2 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -245,6 +245,58 @@ namespace ts.server { event: Event; } + export type Send = (msg: protocol.Message) => void; + + export interface MessageSender extends EventSender { + send: Send; + event: Event; + } + + function defaultSend( + host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + canUseEvents: boolean, + msg: protocol.Message) { + if (msg.type === "event" && !canUseEvents) { + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; + } + host.write(formatMessage(msg, logger, byteLength, host.newLine)); + } + + function defaultEvent( + host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + canUseEvents: boolean, + body: T, eventName: string): void { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + defaultSend(host, byteLength, logger, canUseEvents, ev); + } + + export class DefaultMessageSender implements MessageSender { + constructor(protected host: ServerHost, + protected byteLength: (buf: string, encoding?: string) => number, + protected logger: Logger, + protected canUseEvents: boolean) { } + + public send = (msg: protocol.Message) => { + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + } + + public event = (body: T, eventName: string) => { + defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); + } + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -259,10 +311,9 @@ namespace ts.server { */ canUseEvents: boolean; /** - * An optional callback overriding the default behavior for sending events. - * if set, `canUseEvents` and `eventPort` are ignored. + * An optional callback overriding the default behavior for sending messages. */ - event?: Event; + messageSender?: MessageSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -271,9 +322,7 @@ namespace ts.server { allowLocalPluginLoads?: boolean; } - export class Session implements EventSender { - public readonly event: Event; - + export class Session implements MessageSender { private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -298,23 +347,13 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; - this.canUseEvents = opts.canUseEvents || !!opts.event; + this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; - if (opts.event) { - this.event = opts.event; - } - else { - this.event = function (body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - this.send(ev); - }; + if (opts.messageSender) { + this.send = opts.messageSender.send; + this.event = opts.messageSender.event; } this.eventHandler = this.canUseEvents @@ -340,8 +379,7 @@ namespace ts.server { eventHandler: this.eventHandler, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, - allowLocalPluginLoads: opts.allowLocalPluginLoads, - eventSender: this + allowLocalPluginLoads: opts.allowLocalPluginLoads }; this.projectService = new ProjectService(settings); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); @@ -413,13 +451,11 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event" && !this.canUseEvents) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; - } - this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + } + + public event(body: T, eventName: string): void { + defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); } // For backwards-compatibility only. diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 6fbf093943..cde303bfd3 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -10,7 +10,7 @@ namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService, eventSender?: EventSender): void; + attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } From a7822c550d3d05fdab947d723999144a3806cce4 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:39:02 -0800 Subject: [PATCH 13/21] expose just eventSender --- src/server/server.ts | 62 +++++++++++++++++++++++++++---------------- src/server/session.ts | 62 ++++++++++--------------------------------- 2 files changed, 53 insertions(+), 71 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 2f5061bc71..caf70b9c94 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -518,15 +518,31 @@ namespace ts.server { } } - class SocketEventSender extends DefaultMessageSender { + export class DefaultEventSender implements EventSender { + constructor(protected host: ServerHost, + protected byteLength: (buf: string, encoding?: string) => number, + protected logger: Logger, + protected canUseEvents: boolean) { } + + public event = (body: T, eventName: string) => { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); + } + } + + class SocketEventSender implements EventSender { private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; - constructor(host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, + constructor(private host: ServerHost, + private byteLength: (buf: string, encoding?: string) => number, + private logger: Logger, private eventPort: number) { - super(host, byteLength, logger, /*canUseEvents*/ true); const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; @@ -538,24 +554,24 @@ namespace ts.server { this.socketEventQueue = undefined; } }); - - this.event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - }; } + public event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + }; + private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); } } @@ -563,11 +579,11 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const messageSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultMessageSender(host, Buffer.byteLength, logger, canUseEvents); + const eventSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultEventSender(host, Buffer.byteLength, logger, canUseEvents); const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? messageSender : undefined); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? eventSender : undefined); super({ host, @@ -579,7 +595,7 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - messageSender, + eventSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index ff41138ae2..e917b131bf 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -239,20 +239,12 @@ namespace ts.server { } } - export type Event = (body: T, eventName: string) => void; - export interface EventSender { - event: Event; + event: (body: T, eventName: string) => void; } - export type Send = (msg: protocol.Message) => void; - - export interface MessageSender extends EventSender { - send: Send; - event: Event; - } - - function defaultSend( + /** @internal */ + export function defaultSend( host: ServerHost, byteLength: (buf: string, encoding?: string) => number, logger: Logger, @@ -266,37 +258,6 @@ namespace ts.server { } host.write(formatMessage(msg, logger, byteLength, host.newLine)); } - - function defaultEvent( - host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, - canUseEvents: boolean, - body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - defaultSend(host, byteLength, logger, canUseEvents, ev); - } - - export class DefaultMessageSender implements MessageSender { - constructor(protected host: ServerHost, - protected byteLength: (buf: string, encoding?: string) => number, - protected logger: Logger, - protected canUseEvents: boolean) { } - - public send = (msg: protocol.Message) => { - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); - } - - public event = (body: T, eventName: string) => { - defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); - } - } - export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -313,7 +274,7 @@ namespace ts.server { /** * An optional callback overriding the default behavior for sending messages. */ - messageSender?: MessageSender; + eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -322,7 +283,7 @@ namespace ts.server { allowLocalPluginLoads?: boolean; } - export class Session implements MessageSender { + export class Session implements EventSender { private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -351,9 +312,8 @@ namespace ts.server { const { throttleWaitMilliseconds } = opts; - if (opts.messageSender) { - this.send = opts.messageSender.send; - this.event = opts.messageSender.event; + if (opts.eventSender) { + this.event = opts.eventSender.event; } this.eventHandler = this.canUseEvents @@ -455,7 +415,13 @@ namespace ts.server { } public event(body: T, eventName: string): void { - defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + this.send(ev); } // For backwards-compatibility only. From 1fc113337770d77d9cffbaac72ddb7e12293cb77 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:39:26 -0800 Subject: [PATCH 14/21] baseline accept --- .../reference/api/tsserverlibrary.d.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8ea02a6a5d..6b785153cc 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6909,9 +6909,8 @@ declare namespace ts.server { type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; - type Event = (body: T, eventName: string) => void; interface EventSender { - event: Event; + event: (body: T, eventName: string) => void; } interface SessionOptions { host: ServerHost; @@ -6927,15 +6926,9 @@ declare namespace ts.server { */ canUseEvents: boolean; /** - * If defined, specifies the socket to send events to the client. - * Otherwise, events are sent through the host. + * An optional callback overriding the default behavior for sending messages. */ - eventPort?: number; - /** - * An optional callback overriding the default behavior for sending events. - * if set, `canUseEvents` and `eventPort` are ignored. - */ - event?: Event; + eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6943,7 +6936,6 @@ declare namespace ts.server { allowLocalPluginLoads?: boolean; } class Session implements EventSender { - readonly event: Event; private readonly gcTimer; protected projectService: ProjectService; private changeSeq; @@ -6956,7 +6948,6 @@ declare namespace ts.server { private hrtime; protected logger: Logger; private canUseEvents; - private eventPort; private eventHandler; constructor(opts: SessionOptions); private sendRequestCompletedEvent(requestId); @@ -6964,6 +6955,7 @@ declare namespace ts.server { private projectsUpdatedInBackgroundEvent(openFiles); logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; + event(body: T, eventName: string): void; /** @deprecated */ output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; private doOutput(info, cmdName, reqSeq, success, message?); @@ -7122,7 +7114,7 @@ declare namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService, eventSender?: EventSender): void; + attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } @@ -7483,7 +7475,6 @@ declare namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; - eventSender?: EventSender; } class ProjectService { readonly typingsCache: TypingsCache; From 5c2fea4d4e644302b03a61af5c5a2e3bf38646b2 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:49:34 -0800 Subject: [PATCH 15/21] make eventSender required for TypingsInstaller --- src/server/server.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index caf70b9c94..669ca2869f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,10 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - /** - * If undefined, event-related work will be suppressed. - */ - private eventSender: EventSender | undefined) { + private eventSender: EventSender) { } isKnownTypesPackageName(name: string): boolean { @@ -583,7 +580,7 @@ namespace ts.server { const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? eventSender : undefined); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, eventSender); super({ host, From 49d92d844bc1163ad039366661d8dc2f7817c5f8 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 18:15:39 -0800 Subject: [PATCH 16/21] defer callback and remove handler object --- src/server/server.ts | 160 +++++++++++++++++++++++------------------- src/server/session.ts | 41 ++++------- 2 files changed, 102 insertions(+), 99 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 669ca2869f..4a8ef0c125 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,7 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private eventSender: EventSender) { + private event: Event) { } isKnownTypesPackageName(name: string): boolean { @@ -304,8 +304,8 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.eventSender && this.installer) { - this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); + if (this.event && this.installer) { + this.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } } @@ -416,19 +416,19 @@ namespace ts.server { } case EventInitializationFailed: { - if (!this.eventSender) { + if (!this.event) { break; } const body: protocol.TypesInstallerInitializationFailedEventBody = { message: response.message }; const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case EventBeginInstallTypes: { - if (!this.eventSender) { + if (!this.event) { break; } const body: protocol.BeginInstallTypesEventBody = { @@ -436,12 +436,12 @@ namespace ts.server { packages: response.packagesToInstall, }; const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case EventEndInstallTypes: { - if (!this.eventSender) { + if (!this.event) { break; } if (this.telemetryEnabled) { @@ -454,7 +454,7 @@ namespace ts.server { } }; const eventName: protocol.TelemetryEventName = "telemetry"; - this.eventSender.event(body, eventName); + this.event(body, eventName); } const body: protocol.EndInstallTypesEventBody = { @@ -463,7 +463,7 @@ namespace ts.server { success: response.installSuccess, }; const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case ActionInvalidate: @@ -495,8 +495,8 @@ namespace ts.server { this.projectService.updateTypingsForProject(response); - if (this.eventSender) { - this.eventSender.event(response, "setTypings"); + if (this.event) { + this.event(response, "setTypings"); } break; @@ -515,72 +515,49 @@ namespace ts.server { } } - export class DefaultEventSender implements EventSender { - constructor(protected host: ServerHost, - protected byteLength: (buf: string, encoding?: string) => number, - protected logger: Logger, - protected canUseEvents: boolean) { } + // export class DefaultEventSender implements EventSender { + // constructor(protected host: ServerHost, + // protected byteLength: (buf: string, encoding?: string) => number, + // protected logger: Logger, + // protected canUseEvents: boolean) { } - public event = (body: T, eventName: string) => { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); - } - } - - class SocketEventSender implements EventSender { - private eventSocket: NodeSocket | undefined; - private socketEventQueue: { body: any, eventName: string }[] | undefined; - - constructor(private host: ServerHost, - private byteLength: (buf: string, encoding?: string) => number, - private logger: Logger, - private eventPort: number) { - - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.body, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - } - - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - }; - - private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); - } - } + // public event = (body: T, eventName: string) => { + // const ev: protocol.Event = { + // seq: 0, + // type: "event", + // event: eventName, + // body + // }; + // defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); + // } + // } class IOSession extends Session { + private eventPort: number; + private eventSocket: NodeSocket | undefined; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + private constructed: boolean | undefined; + constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const eventSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultEventSender(host, Buffer.byteLength, logger, canUseEvents); + const event: Event | undefined = canUseEvents ? + (body: {}, eventName: string) => { + if (this.constructed) { + this.event(body, eventName); + } + else { + // It is unsafe to dereference `this` before initialization completes, + // so we defer until the next tick. + // + // Construction should finish before the next tick fires, so we do not need to do this recursively. + setImmediate(() => this.event(body, eventName)); + } + } : undefined; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, eventSender); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, event); super({ host, @@ -592,10 +569,51 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - eventSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, - allowLocalPluginLoads: options.allowLocalPluginLoads }); + allowLocalPluginLoads: options.allowLocalPluginLoads + }); + + this.eventPort = eventPort; + if (this.canUseEvents && this.eventPort) { + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + this.constructed = true; + } + + event(body: T, eventName: string): void { + Debug.assert(this.constructed, "Should only call `IOSession.prototype.event` on an initialized IOSession"); + + if (this.canUseEvents && this.eventPort) { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + else { + super.event(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); } exit() { diff --git a/src/server/session.ts b/src/server/session.ts index e917b131bf..d469e5bc8a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -239,25 +239,12 @@ namespace ts.server { } } + export type Event = (body: T, eventName: string) => void; + export interface EventSender { - event: (body: T, eventName: string) => void; + event: Event; } - /** @internal */ - export function defaultSend( - host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, - canUseEvents: boolean, - msg: protocol.Message) { - if (msg.type === "event" && !canUseEvents) { - if (logger.hasLevel(LogLevel.verbose)) { - logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; - } - host.write(formatMessage(msg, logger, byteLength, host.newLine)); - } export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -271,10 +258,6 @@ namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * An optional callback overriding the default behavior for sending messages. - */ - eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -291,14 +274,14 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private host: ServerHost; + protected host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; - private byteLength: (buf: string, encoding?: string) => number; + protected byteLength: (buf: string, encoding?: string) => number; private hrtime: (start?: number[]) => number[]; protected logger: Logger; - private canUseEvents: boolean; + protected canUseEvents: boolean; private eventHandler: ProjectServiceEventHandler; constructor(opts: SessionOptions) { @@ -312,10 +295,6 @@ namespace ts.server { const { throttleWaitMilliseconds } = opts; - if (opts.eventSender) { - this.event = opts.eventSender.event; - } - this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; @@ -411,7 +390,13 @@ namespace ts.server { } public send(msg: protocol.Message) { - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + if (msg.type === "event" && !this.canUseEvents) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; + } + this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } public event(body: T, eventName: string): void { From cda486ad85c611b448c2025171c40e13b53b1208 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 18:27:30 -0800 Subject: [PATCH 17/21] update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6b785153cc..3ac976f222 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6909,8 +6909,9 @@ declare namespace ts.server { type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; + type Event = (body: T, eventName: string) => void; interface EventSender { - event: (body: T, eventName: string) => void; + event: Event; } interface SessionOptions { host: ServerHost; @@ -6925,10 +6926,6 @@ declare namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * An optional callback overriding the default behavior for sending messages. - */ - eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6941,13 +6938,13 @@ declare namespace ts.server { private changeSeq; private currentRequestId; private errorCheck; - private host; + protected host: ServerHost; private readonly cancellationToken; protected readonly typingsInstaller: ITypingsInstaller; - private byteLength; + protected byteLength: (buf: string, encoding?: string) => number; private hrtime; protected logger: Logger; - private canUseEvents; + protected canUseEvents: boolean; private eventHandler; constructor(opts: SessionOptions); private sendRequestCompletedEvent(requestId); From c098a5cf83e7071dec2b5a3985d706fa43ad61b1 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:06:08 -0800 Subject: [PATCH 18/21] type `event` callback correctly --- src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.ts b/src/server/server.ts index 4a8ef0c125..68bf4384f9 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,7 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private event: Event) { + private event: Event | undefined) { } isKnownTypesPackageName(name: string): boolean { From 8b0d3ab31894dfdd8c69a6b4e47f27b8389a57f0 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:10:38 -0800 Subject: [PATCH 19/21] remove comments --- src/server/server.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 68bf4384f9..b49a74b5d8 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -515,23 +515,6 @@ namespace ts.server { } } - // export class DefaultEventSender implements EventSender { - // constructor(protected host: ServerHost, - // protected byteLength: (buf: string, encoding?: string) => number, - // protected logger: Logger, - // protected canUseEvents: boolean) { } - - // public event = (body: T, eventName: string) => { - // const ev: protocol.Event = { - // seq: 0, - // type: "event", - // event: eventName, - // body - // }; - // defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); - // } - // } - class IOSession extends Session { private eventPort: number; private eventSocket: NodeSocket | undefined; From 890820b52893d6d86483bb1647fb3b2d48191383 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:19:05 -0800 Subject: [PATCH 20/21] cleanup NodeTypingsInstaller --- src/server/server.ts | 177 +++++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 100 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index b49a74b5d8..b69884c42c 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -246,7 +246,6 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; - private installerPidReported = false; private projectService: ProjectService; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; @@ -272,7 +271,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private event: Event | undefined) { + private event: Event) { } isKnownTypesPackageName(name: string): boolean { @@ -300,17 +299,6 @@ namespace ts.server { }); } - private reportInstallerProcessId() { - if (this.installerPidReported) { - return; - } - if (this.event && this.installer) { - this.event({ pid: this.installer.pid }, "typingsInstallerPid"); - this.installerPidReported = true; - } - } - - attach(projectService: ProjectService) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { @@ -350,7 +338,8 @@ namespace ts.server { this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv }); this.installer.on("message", m => this.handleMessage(m)); - this.reportInstallerProcessId(); + + this.event({ pid: this.installer.pid }, "typingsInstallerPid"); process.on("exit", () => { this.installer.kill(); @@ -415,92 +404,81 @@ namespace ts.server { break; } case EventInitializationFailed: - { - if (!this.event) { - break; - } - const body: protocol.TypesInstallerInitializationFailedEventBody = { - message: response.message - }; - const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; - this.event(body, eventName); - break; - } - case EventBeginInstallTypes: - { - if (!this.event) { - break; - } - const body: protocol.BeginInstallTypesEventBody = { - eventId: response.eventId, - packages: response.packagesToInstall, - }; - const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; - this.event(body, eventName); - break; - } - case EventEndInstallTypes: - { - if (!this.event) { - break; - } - if (this.telemetryEnabled) { - const body: protocol.TypingsInstalledTelemetryEventBody = { - telemetryEventName: "typingsInstalled", - payload: { - installedPackages: response.packagesToInstall.join(","), - installSuccess: response.installSuccess, - typingsInstallerVersion: response.typingsInstallerVersion - } + { + const body: protocol.TypesInstallerInitializationFailedEventBody = { + message: response.message }; - const eventName: protocol.TelemetryEventName = "telemetry"; + const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; this.event(body, eventName); + break; } + case EventBeginInstallTypes: + { + const body: protocol.BeginInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + }; + const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; + this.event(body, eventName); + break; + } + case EventEndInstallTypes: + { + if (this.telemetryEnabled) { + const body: protocol.TypingsInstalledTelemetryEventBody = { + telemetryEventName: "typingsInstalled", + payload: { + installedPackages: response.packagesToInstall.join(","), + installSuccess: response.installSuccess, + typingsInstallerVersion: response.typingsInstallerVersion + } + }; + const eventName: protocol.TelemetryEventName = "telemetry"; + this.event(body, eventName); + } - const body: protocol.EndInstallTypesEventBody = { - eventId: response.eventId, - packages: response.packagesToInstall, - success: response.installSuccess, - }; - const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; - this.event(body, eventName); - break; - } + const body: protocol.EndInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + success: response.installSuccess, + }; + const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; + this.event(body, eventName); + break; + } case ActionInvalidate: - { - this.projectService.updateTypingsForProject(response); - break; - } + { + this.projectService.updateTypingsForProject(response); + break; + } case ActionSet: - { - if (this.activeRequestCount > 0) { - this.activeRequestCount--; - } - else { - Debug.fail("Received too many responses"); - } - - while (this.requestQueue.length > 0) { - const queuedRequest = this.requestQueue.shift(); - if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) { - this.requestMap.delete(queuedRequest.operationId); - this.scheduleRequest(queuedRequest); - break; + { + if (this.activeRequestCount > 0) { + this.activeRequestCount--; + } + else { + Debug.fail("Received too many responses"); } - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`); + while (this.requestQueue.length > 0) { + const queuedRequest = this.requestQueue.shift(); + if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) { + this.requestMap.delete(queuedRequest.operationId); + this.scheduleRequest(queuedRequest); + break; + } + + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`); + } } - } - this.projectService.updateTypingsForProject(response); + this.projectService.updateTypingsForProject(response); - if (this.event) { this.event(response, "setTypings"); - } - break; - } + break; + } default: assertTypeIsNever(response); } @@ -524,19 +502,18 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const event: Event | undefined = canUseEvents ? - (body: {}, eventName: string) => { - if (this.constructed) { - this.event(body, eventName); - } - else { - // It is unsafe to dereference `this` before initialization completes, - // so we defer until the next tick. - // - // Construction should finish before the next tick fires, so we do not need to do this recursively. - setImmediate(() => this.event(body, eventName)); - } - } : undefined; + const event: Event | undefined = (body: {}, eventName: string) => { + if (this.constructed) { + this.event(body, eventName); + } + else { + // It is unsafe to dereference `this` before initialization completes, + // so we defer until the next tick. + // + // Construction should finish before the next tick fires, so we do not need to do this recursively. + setImmediate(() => this.event(body, eventName)); + } + }; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined From d2cc4f15bec6cacaf6cc297acd870aaa5807a4bc Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Fri, 17 Nov 2017 19:48:57 -0800 Subject: [PATCH 21/21] test overriding Session.event --- .../unittests/tsserverProjectSystem.ts | 122 +++++++++--------- src/server/server.ts | 2 +- src/server/session.ts | 18 ++- 3 files changed, 70 insertions(+), 72 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ea573726eb..b112d46cec 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -210,6 +210,8 @@ namespace ts.projectSystem { class TestSession extends server.Session { private seq = 0; + public events: protocol.Event[] = []; + public host: TestServerHost; getProjectService() { return this.projectService; @@ -229,6 +231,16 @@ namespace ts.projectSystem { request.type = "request"; return this.executeCommand(request); } + + public event(body: T, eventName: string) { + this.events.push(server.toEvent(eventName, body)); + super.event(body, eventName); + } + + public clearMessages() { + clear(this.events); + this.host.clearOutput(); + } } export function createSession(host: server.ServerHost, opts: Partial = {}) { @@ -436,48 +448,29 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: TestServerHost) { - assert.equal(actualOutput, server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, host.newLine)); + function checkErrorMessage(session: TestSession, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { + checkNthEvent(session, ts.server.toEvent(eventName, diagnostics), 0, /*isMostRecent*/ false); } - function checkErrorMessage(host: TestServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { - const outputs = host.getOutput(); - assert.isTrue(outputs.length >= 1, outputs.toString()); - const event: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body: diagnostics - }; - assertEvent(outputs[0], event, host); + function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number) { + checkNthEvent(session, ts.server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, /*isMostRecent*/ true); } - function checkCompleteEvent(host: TestServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) { - const outputs = host.getOutput(); - assert.equal(outputs.length, numberOfCurrentEvents, outputs.toString()); - const event: protocol.RequestCompletedEvent = { - seq: 0, - type: "event", - event: "requestCompleted", - body: { - request_seq: expectedSequenceId - } - }; - assertEvent(outputs[numberOfCurrentEvents - 1], event, host); + function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + checkNthEvent(session, ts.server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); } - function checkProjectUpdatedInBackgroundEvent(host: TestServerHost, openFiles: string[]) { - const outputs = host.getOutput(); - assert.equal(outputs.length, 1, outputs.toString()); - const event: protocol.ProjectsUpdatedInBackgroundEvent = { - seq: 0, - type: "event", - event: "projectsUpdatedInBackground", - body: { - openFiles - } - }; - assertEvent(outputs[0], event, host); + function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + const events = session.events; + assert.deepEqual(events[index], expectedEvent); + + const outputs = session.host.getOutput(); + assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + + if (isMostRecent) { + assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + } } describe("tsserverProjectSystem", () => { @@ -2887,14 +2880,14 @@ namespace ts.projectSystem { assert.isFalse(hasError); host.checkTimeoutQueueLength(2); - checkErrorMessage(host, "syntaxDiag", { file: untitledFile, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); assert.isFalse(hasError); - checkErrorMessage(host, "semanticDiag", { file: untitledFile, diagnostics: [] }); + checkErrorMessage(session, "semanticDiag", { file: untitledFile, diagnostics: [] }); - checkCompleteEvent(host, 2, expectedSequenceId); + checkCompleteEvent(session, 2, expectedSequenceId); } it("has projectRoot", () => { @@ -2938,7 +2931,7 @@ namespace ts.projectSystem { verifyErrorsInApp(); function verifyErrorsInApp() { - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -2948,13 +2941,13 @@ namespace ts.projectSystem { } }); host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(host, "syntaxDiag", { file: app.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); - checkErrorMessage(host, "semanticDiag", { file: app.path, diagnostics: [] }); - checkCompleteEvent(host, 2, expectedSequenceId); - host.clearOutput(); + checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); } }); }); @@ -3679,7 +3672,7 @@ namespace ts.projectSystem { } }); checkNumberOfProjects(service, { inferredProjects: 1 }); - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -3690,23 +3683,24 @@ namespace ts.projectSystem { }); host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(host, "syntaxDiag", { file: file1.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); const moduleNotFound = Diagnostics.Cannot_find_module_0; const startOffset = file1.content.indexOf('"') + 1; - checkErrorMessage(host, "semanticDiag", { + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [{ start: { line: 1, offset: startOffset }, end: { line: 1, offset: startOffset + '"pad"'.length }, text: formatStringFromArgs(moduleNotFound.message, ["pad"]), code: moduleNotFound.code, - category: DiagnosticCategory[moduleNotFound.category].toLowerCase() + category: DiagnosticCategory[moduleNotFound.category].toLowerCase(), + source: undefined }] }); - checkCompleteEvent(host, 2, expectedSequenceId); - host.clearOutput(); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); const padIndex: FileOrFolder = { path: `${folderPath}/node_modules/@types/pad/index.d.ts`, @@ -3715,15 +3709,15 @@ namespace ts.projectSystem { files.push(padIndex); host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); host.runQueuedTimeoutCallbacks(); - checkProjectUpdatedInBackgroundEvent(host, [file1.path]); - host.clearOutput(); + checkProjectUpdatedInBackgroundEvent(session, [file1.path]); + session.clearMessages(); host.runQueuedTimeoutCallbacks(); - checkErrorMessage(host, "syntaxDiag", { file: file1.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); - checkErrorMessage(host, "semanticDiag", { file: file1.path, diagnostics: [] }); + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); }); }); @@ -4837,7 +4831,7 @@ namespace ts.projectSystem { command: "projectInfo", arguments: { file: f1.path } }); - host.clearOutput(); + session.clearMessages(); // cancel previously issued Geterr cancellationToken.setRequestToCancel(getErrId); @@ -4861,7 +4855,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); cancellationToken.setRequestToCancel(getErrId); host.runQueuedImmediateCallbacks(); @@ -4883,7 +4877,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); // the semanticDiag message host.runQueuedImmediateCallbacks(); @@ -4906,7 +4900,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); session.executeCommandSeq({ command: "geterr", @@ -4920,7 +4914,7 @@ namespace ts.projectSystem { const event = getMessage(n); assert.equal(event.event, "requestCompleted"); assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); - host.clearOutput(); + session.clearMessages(); } function getMessage(n: number) { @@ -6423,7 +6417,7 @@ namespace ts.projectSystem { }); // Verified the events, reset them - host.clearOutput(); + session.clearMessages(); } } }); diff --git a/src/server/server.ts b/src/server/server.ts index b69884c42c..fb0b3b781a 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -573,7 +573,7 @@ namespace ts.server { } private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage(toEvent(body, eventName), this.logger, this.byteLength, this.host.newLine), "utf8"); } exit() { diff --git a/src/server/session.ts b/src/server/session.ts index d469e5bc8a..ab1bedc7b6 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -245,6 +245,16 @@ namespace ts.server { event: Event; } + /** @internal */ + export function toEvent(eventName: string, body: {}): protocol.Event { + return { + seq: 0, + type: "event", + event: eventName, + body + }; + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -400,13 +410,7 @@ namespace ts.server { } public event(body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - this.send(ev); + this.send(toEvent(eventName, body)); } // For backwards-compatibility only.