diff --git a/extensions/typescript-language-features/src/test/cachedResponse.test.ts b/extensions/typescript-language-features/src/test/cachedResponse.test.ts index d4d0ace4db7..e2b4234dd45 100644 --- a/extensions/typescript-language-features/src/test/cachedResponse.test.ts +++ b/extensions/typescript-language-features/src/test/cachedResponse.test.ts @@ -15,11 +15,10 @@ suite('CachedResponse', () => { const doc = await vscode.workspace.openTextDocument({ language: 'javascript', content: '' }); const response = new CachedResponse(); - let seq = 0; - const makeRequest = async () => createResponse(`test-${seq++}`); + const responder = createSequentialResponder('test'); - assertResult(await response.execute(doc, makeRequest), 'test-0'); - assertResult(await response.execute(doc, makeRequest), 'test-0'); + assertResult(await response.execute(doc, responder), 'test-0'); + assertResult(await response.execute(doc, responder), 'test-0'); }); test('should invalidate cache for new document', async () => { @@ -27,29 +26,32 @@ suite('CachedResponse', () => { const doc2 = await vscode.workspace.openTextDocument({ language: 'javascript', content: '' }); const response = new CachedResponse(); - let seq = 0; - const makeRequest = async () => createResponse(`test-${seq++}`); + const responder = createSequentialResponder('test'); - assertResult(await response.execute(doc1, makeRequest), 'test-0'); - assertResult(await response.execute(doc1, makeRequest), 'test-0'); - assertResult(await response.execute(doc2, makeRequest), 'test-1'); - assertResult(await response.execute(doc2, makeRequest), 'test-1'); - assertResult(await response.execute(doc1, makeRequest), 'test-2'); - assertResult(await response.execute(doc1, makeRequest), 'test-2'); + assertResult(await response.execute(doc1, responder), 'test-0'); + assertResult(await response.execute(doc1, responder), 'test-0'); + assertResult(await response.execute(doc2, responder), 'test-1'); + assertResult(await response.execute(doc2, responder), 'test-1'); + assertResult(await response.execute(doc1, responder), 'test-2'); + assertResult(await response.execute(doc1, responder), 'test-2'); }); - test('should not cache canceled response', async () => { + test('should not cache cancelled responses', async () => { const doc = await vscode.workspace.openTextDocument({ language: 'javascript', content: '' }); const response = new CachedResponse(); - let seq = 0; - const makeRequest = async () => createResponse(`test-${seq++}`); + const responder = createSequentialResponder('test'); - const result1 = await response.execute(doc, async () => new CancelledResponse('cancleed')); - assert.strictEqual(result1.type, 'cancelled'); + const cancelledResponder = createEventualResponder(); + const result1 = response.execute(doc, () => cancelledResponder.promise); + const result2 = response.execute(doc, responder); + const result3 = response.execute(doc, responder); - assertResult(await response.execute(doc, makeRequest), 'test-0'); - assertResult(await response.execute(doc, makeRequest), 'test-0'); + cancelledResponder.resolve(new CancelledResponse('cancelled')); + + assert.strictEqual((await result1).type, 'cancelled'); + assertResult(await result2, 'test-0'); + assertResult(await result3, 'test-0'); }); }); @@ -72,3 +74,13 @@ function createResponse(command: string): Proto.Response { }; } +function createSequentialResponder(prefix: string) { + let count = 0; + return async () => createResponse(`${prefix}-${count++}`); +} + +function createEventualResponder(): {promise: Promise, resolve: (x: T) => void } { + let resolve: (value: T) => void; + const promise = new Promise(r => { resolve = r; }); + return { promise, resolve: resolve! }; +} diff --git a/extensions/typescript-language-features/src/tsServer/cachedResponse.ts b/extensions/typescript-language-features/src/tsServer/cachedResponse.ts index 71572c67a96..ea8414cf8a5 100644 --- a/extensions/typescript-language-features/src/tsServer/cachedResponse.ts +++ b/extensions/typescript-language-features/src/tsServer/cachedResponse.ts @@ -7,41 +7,42 @@ import * as vscode from 'vscode'; import * as Proto from '../protocol'; import { ServerResponse } from '../typescriptService'; +type Resolve = () => Promise>; + +/** + * Caches a class of TS Server request based on document. + */ export class CachedResponse { private response?: Promise>; private version: number = -1; private document: string = ''; + /** + * Execute a request. May return cached value or resolve the new value + * + * Caller must ensure that all input `resolve` functions return equivilent results (keyed only off of document). + */ public execute( document: vscode.TextDocument, - f: () => Promise> + resolve: Resolve ): Promise> { if (this.response && this.matches(document)) { - return this.response.then(result => result.type === 'cancelled' ? f() : result); + // Chain so that on cancellation we fall back to the next resolve + return this.response = this.response.then(result => result.type === 'cancelled' ? resolve() : result); } - return this.update(document, f()); + return this.reset(document, resolve); } private matches(document: vscode.TextDocument): boolean { return this.version === document.version && this.document === document.uri.toString(); } - private async update( + private async reset( document: vscode.TextDocument, - response: Promise> + resolve: Resolve ): Promise> { - this.response = response; this.version = document.version; this.document = document.uri.toString(); - - const result = await response; - if (this.matches(document)) { - if (result.type === 'cancelled') { - // invalidate - this.version = -1; - this.document = ''; - } - } - return result; + return this.response = resolve(); } }