mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-10-28 16:36:04 +08:00
Add defer functionality to generator executor (#64157)
This commit is contained in:
1 parent
ee72eb0e48
commit
10404f71a8
4 files changed
+427
-93
No files matched your search
@@ -221,8 +221,8 @@ export interface TranspileOutput {
|
||||
}
|
||||
|
||||
// @sync-only-start
|
||||
// export { all } from "./generatorSupport.ts";
|
||||
// import {all, type ExecutedGeneratorsResults, type APIRequestGenerator} from "./generatorSupport.ts";
|
||||
// export { all, defer, type APIRequestGenerator, type AnyAPIRequestGenerator, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts";
|
||||
// import {executeRequestGenerators, type ExecutedGeneratorsResults, type AnyAPIRequestGenerator} from "./generatorSupport.ts";
|
||||
// @sync-only-end
|
||||
|
||||
export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHost {
|
||||
@@ -259,13 +259,8 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
|
||||
}
|
||||
// @sync-skip-block-end
|
||||
// @sync-only-start
|
||||
// batch<T extends readonly APIRequestGenerator[]>(...requestGenerators: T): ExecutedGeneratorsResults<T> {
|
||||
// const batches = all(...requestGenerators);
|
||||
// let state = batches.next();
|
||||
// while (!state.done) {
|
||||
// state = batches.next(this.client.batchRequests(state.value).responses);
|
||||
// }
|
||||
// return state.value;
|
||||
// batch<T extends readonly AnyAPIRequestGenerator[]>(...requestGenerators: T): ExecutedGeneratorsResults<T> {
|
||||
// return executeRequestGenerators(requestGenerators, requests => this.client.batchRequests(requests).responses);
|
||||
// }
|
||||
// @sync-only-end
|
||||
|
||||
|
||||
@@ -237,11 +237,11 @@ export interface TranspileOutput {
|
||||
sourceMapText?: string;
|
||||
}
|
||||
|
||||
export { all } from "./generatorSupport.ts";
|
||||
export { all, type AnyAPIRequestGenerator, type APIRequestGenerator, defer, type DeferredAPIRequestGenerator, type ExecutedGeneratorsResults } from "./generatorSupport.ts";
|
||||
import {
|
||||
all,
|
||||
type APIRequestGenerator,
|
||||
type AnyAPIRequestGenerator,
|
||||
type ExecutedGeneratorsResults,
|
||||
executeRequestGenerators,
|
||||
} from "./generatorSupport.ts";
|
||||
|
||||
export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHost {
|
||||
@@ -287,13 +287,8 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
|
||||
);
|
||||
}
|
||||
|
||||
batch<T extends readonly APIRequestGenerator[]>(...requestGenerators: T): ExecutedGeneratorsResults<T> {
|
||||
const batches = all(...requestGenerators);
|
||||
let state = batches.next();
|
||||
while (!state.done) {
|
||||
state = batches.next(this.client.batchRequests(state.value).responses);
|
||||
}
|
||||
return state.value;
|
||||
batch<T extends readonly AnyAPIRequestGenerator[]>(...requestGenerators: T): ExecutedGeneratorsResults<T> {
|
||||
return executeRequestGenerators(requestGenerators, requests => this.client.batchRequests(requests).responses);
|
||||
}
|
||||
|
||||
private get ensureInitialized(): {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
APIResponse,
|
||||
} from "../proto.ts";
|
||||
|
||||
export function cacheGeneratorMethod<Sync extends (...args: any[]) => any, Gen extends (...args: any[]) => APIRequestGenerator>(
|
||||
export function cacheGeneratorMethod<Sync extends (...args: any[]) => any, Gen extends (...args: any[]) => AnyAPIRequestGenerator>(
|
||||
owner: object,
|
||||
name: PropertyKey,
|
||||
sync: Sync,
|
||||
@@ -22,82 +22,144 @@ export function apiRequest<Method extends keyof APIMethodInfo>(
|
||||
export function* apiRequest(method: PropertyKey, params: unknown): Generator<{ method: PropertyKey; params: unknown; }, unknown, unknown> {
|
||||
return yield { method, params };
|
||||
}
|
||||
export type APIRequestGenerator<Return = any> = Generator<APIRequest | readonly APIRequest[], Return, any>;
|
||||
const deferredGeneratorMarker: unique symbol = Symbol();
|
||||
interface DeferredAPIRequest {
|
||||
readonly deferred: APIRequestGenerator;
|
||||
}
|
||||
type APIRequestGeneratorYield = APIRequest | readonly APIRequest[] | DeferredAPIRequest;
|
||||
export type APIRequestGenerator<Return = any> = Generator<APIRequestGeneratorYield, Return, any>;
|
||||
export type DeferredAPIRequestGenerator = Generator<DeferredAPIRequest, void, unknown> & { readonly [deferredGeneratorMarker]: true; };
|
||||
export type AnyAPIRequestGenerator<Return = any> = APIRequestGenerator<Return> | DeferredAPIRequestGenerator;
|
||||
type GeneratorReturn<T> = T extends Generator<any, infer R, any> ? R : never;
|
||||
export type ExecutedGeneratorsResults<T extends readonly APIRequestGenerator[]> = {
|
||||
[K in keyof T]: GeneratorReturn<T[K]>;
|
||||
};
|
||||
export type ExecutedGeneratorsResults<T extends readonly AnyAPIRequestGenerator[]> = number extends T["length"] ? GeneratorReturn<Exclude<T[number], DeferredAPIRequestGenerator>>[]
|
||||
: T extends readonly [infer Head extends AnyAPIRequestGenerator, ...infer Tail extends readonly AnyAPIRequestGenerator[]] ? Head extends DeferredAPIRequestGenerator ? ExecutedGeneratorsResults<Tail> : [GeneratorReturn<Head>, ...ExecutedGeneratorsResults<Tail>]
|
||||
: [];
|
||||
|
||||
interface GeneratorResponse {
|
||||
result: unknown;
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
export function all<const T extends readonly APIRequestGenerator[]>(
|
||||
...requestGenerators: T
|
||||
): Generator<APIRequest[], ExecutedGeneratorsResults<T>, readonly GeneratorResponse[]>;
|
||||
export function* all<T extends readonly APIRequestGenerator[]>(
|
||||
...requestGenerators: T
|
||||
): Generator<APIRequest[], ExecutedGeneratorsResults<T>, readonly GeneratorResponse[]> {
|
||||
const results: any[] = [];
|
||||
const requestObjects: (APIRequest | readonly APIRequest[] | undefined)[] = [];
|
||||
const completedIndices = new Set<number>();
|
||||
for (let i = 0; i < requestGenerators.length; i++) {
|
||||
const state = requestGenerators[i].next();
|
||||
interface RequestRunnerOptions {
|
||||
executeDeferred?: boolean;
|
||||
}
|
||||
|
||||
function createRequestRunner<T extends readonly AnyAPIRequestGenerator[]>(requestGenerators: T, options: RequestRunnerOptions = {}) {
|
||||
const registeredGenerators = new Set<AnyAPIRequestGenerator>();
|
||||
const requestsByGenerator = new Map<AnyAPIRequestGenerator, APIRequestGeneratorYield>();
|
||||
const resultsByGenerator = new Map<AnyAPIRequestGenerator, unknown>();
|
||||
for (const generator of requestGenerators) {
|
||||
addGenerator(generator);
|
||||
}
|
||||
const requestRounds = runRequestRounds();
|
||||
return { requestRounds, getResults };
|
||||
|
||||
function advanceGenerator(generator: AnyAPIRequestGenerator, value?: unknown, error?: string): void {
|
||||
let state = error === undefined
|
||||
? generator.next(value)
|
||||
: generator.throw(new Error(error));
|
||||
while (!state.done && isDeferredAPIRequest(state.value) && options.executeDeferred) {
|
||||
addGenerator(state.value.deferred);
|
||||
state = generator.next();
|
||||
}
|
||||
if (state.done) {
|
||||
results[i] = state.value;
|
||||
completedIndices.add(i);
|
||||
requestsByGenerator.delete(generator);
|
||||
resultsByGenerator.set(generator, state.value);
|
||||
}
|
||||
else {
|
||||
requestObjects[i] = state.value;
|
||||
requestsByGenerator.set(generator, state.value);
|
||||
}
|
||||
}
|
||||
while (completedIndices.size < requestGenerators.length) {
|
||||
const requests: APIRequest[] = [];
|
||||
const responseIndices: (number | readonly number[] | undefined)[] = [];
|
||||
const responseIndexByDeduplicationKey = new Map<string, number>();
|
||||
const addRequest = (request: APIRequest): number => {
|
||||
const deduplicationKey = getRequestDeduplicationKey(request);
|
||||
let responseIndex = deduplicationKey === undefined ? undefined : responseIndexByDeduplicationKey.get(deduplicationKey);
|
||||
if (responseIndex === undefined) {
|
||||
responseIndex = requests.length;
|
||||
requests.push(request);
|
||||
if (deduplicationKey !== undefined) responseIndexByDeduplicationKey.set(deduplicationKey, responseIndex);
|
||||
}
|
||||
return responseIndex;
|
||||
};
|
||||
for (let i = 0; i < requestGenerators.length; i++) {
|
||||
if (completedIndices.has(i)) continue;
|
||||
|
||||
const request = requestObjects[i]!;
|
||||
responseIndices[i] = isRequestGroup(request) ? request.map(addRequest) : addRequest(request);
|
||||
}
|
||||
function addGenerator(generator: AnyAPIRequestGenerator): void {
|
||||
if (registeredGenerators.has(generator)) throw new Error("Cannot execute the same generator instance more than once");
|
||||
registeredGenerators.add(generator);
|
||||
advanceGenerator(generator);
|
||||
}
|
||||
|
||||
const responses = yield requests;
|
||||
for (let i = 0; i < requestGenerators.length; i++) {
|
||||
if (completedIndices.has(i)) continue;
|
||||
function* runRequestRounds(): Generator<APIRequest[] | DeferredAPIRequest, void, readonly GeneratorResponse[]> {
|
||||
while (requestsByGenerator.size) {
|
||||
for (const generator of registeredGenerators) {
|
||||
let request = requestsByGenerator.get(generator);
|
||||
while (request && isDeferredAPIRequest(request)) {
|
||||
yield request;
|
||||
advanceGenerator(generator);
|
||||
request = requestsByGenerator.get(generator);
|
||||
}
|
||||
}
|
||||
if (!requestsByGenerator.size) break;
|
||||
|
||||
const requestGenerator = requestGenerators[i];
|
||||
const responseIndex = responseIndices[i]!;
|
||||
let state: IteratorResult<APIRequest | readonly APIRequest[], any>;
|
||||
if (typeof responseIndex === "number") {
|
||||
const result = responses[responseIndex];
|
||||
state = result.error
|
||||
? requestGenerator.throw(new Error(result.error))
|
||||
: requestGenerator.next(result.result);
|
||||
const requests: APIRequest[] = [];
|
||||
const responseIndexByDeduplicationKey = new Map<string, number>();
|
||||
const addRequest = (request: APIRequest): number => {
|
||||
const deduplicationKey = getRequestDeduplicationKey(request);
|
||||
let responseIndex = deduplicationKey === undefined ? undefined : responseIndexByDeduplicationKey.get(deduplicationKey);
|
||||
if (responseIndex === undefined) {
|
||||
responseIndex = requests.length;
|
||||
requests.push(request);
|
||||
if (deduplicationKey !== undefined) responseIndexByDeduplicationKey.set(deduplicationKey, responseIndex);
|
||||
}
|
||||
return responseIndex;
|
||||
};
|
||||
// TODO: Use Iterator.prototype.filter when target >= ES2025
|
||||
const roundGenerators = [...registeredGenerators].filter(generator => requestsByGenerator.has(generator));
|
||||
const responseIndices = new Map<AnyAPIRequestGenerator, number | readonly number[]>();
|
||||
for (const generator of roundGenerators) {
|
||||
const request = requestsByGenerator.get(generator) as APIRequest | readonly APIRequest[];
|
||||
responseIndices.set(generator, isRequestGroup(request) ? request.map(addRequest) : addRequest(request));
|
||||
}
|
||||
else {
|
||||
state = requestGenerator.next(responseIndex.map(index => responses[index]));
|
||||
|
||||
const responses = yield requests;
|
||||
for (const generator of roundGenerators) {
|
||||
const responseIndex = responseIndices.get(generator)!;
|
||||
if (typeof responseIndex === "number") {
|
||||
const result = responses[responseIndex];
|
||||
advanceGenerator(generator, result.result, result.error || undefined);
|
||||
}
|
||||
else {
|
||||
advanceGenerator(generator, responseIndex.map(index => responses[index]));
|
||||
}
|
||||
}
|
||||
if (state.done) {
|
||||
results[i] = state.value;
|
||||
completedIndices.add(i);
|
||||
requestObjects[i] = undefined;
|
||||
}
|
||||
else requestObjects[i] = state.value;
|
||||
}
|
||||
}
|
||||
return results as any[] as ExecutedGeneratorsResults<T>;
|
||||
|
||||
function getResults(): ExecutedGeneratorsResults<T> {
|
||||
return requestGenerators
|
||||
.filter(generator => !isDeferredGenerator(generator))
|
||||
.map(generator => resultsByGenerator.get(generator)) as ExecutedGeneratorsResults<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export function all<const T extends readonly AnyAPIRequestGenerator[]>(
|
||||
...requestGenerators: T
|
||||
): APIRequestGenerator<ExecutedGeneratorsResults<T>>;
|
||||
export function* all<T extends readonly AnyAPIRequestGenerator[]>(
|
||||
...requestGenerators: T
|
||||
): APIRequestGenerator<ExecutedGeneratorsResults<T>> {
|
||||
const { requestRounds, getResults } = createRequestRunner(requestGenerators);
|
||||
yield* requestRounds;
|
||||
return getResults();
|
||||
}
|
||||
|
||||
export function executeRequestGenerators<T extends readonly AnyAPIRequestGenerator[]>(
|
||||
requestGenerators: T,
|
||||
executeRequests: (requests: APIRequest[]) => readonly GeneratorResponse[],
|
||||
): ExecutedGeneratorsResults<T> {
|
||||
const { requestRounds, getResults } = createRequestRunner(requestGenerators, { executeDeferred: true });
|
||||
let state = requestRounds.next();
|
||||
while (!state.done) {
|
||||
if (isDeferredAPIRequest(state.value)) throw new Error("Unexpected deferred request");
|
||||
state = requestRounds.next(executeRequests(state.value));
|
||||
}
|
||||
return getResults();
|
||||
}
|
||||
|
||||
function isDeferredAPIRequest(request: APIRequestGeneratorYield): request is DeferredAPIRequest {
|
||||
return !Array.isArray(request) && "deferred" in request;
|
||||
}
|
||||
|
||||
function isDeferredGenerator(generator: AnyAPIRequestGenerator): generator is DeferredAPIRequestGenerator {
|
||||
return deferredGeneratorMarker in generator;
|
||||
}
|
||||
|
||||
function isRequestGroup(request: APIRequest | readonly APIRequest[]): request is readonly APIRequest[] {
|
||||
@@ -112,3 +174,11 @@ function getRequestDeduplicationKey(request: APIRequest): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function defer(gen: APIRequestGenerator): DeferredAPIRequestGenerator {
|
||||
const deferred = (function* (): Generator<DeferredAPIRequest, void, unknown> {
|
||||
yield { deferred: gen };
|
||||
})() as DeferredAPIRequestGenerator;
|
||||
Object.defineProperty(deferred, deferredGeneratorMarker, { value: true });
|
||||
return deferred;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
all,
|
||||
type API,
|
||||
type ConditionalType,
|
||||
defer,
|
||||
type IndexedAccessType,
|
||||
type IndexInfo,
|
||||
type InterfaceType,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
describe,
|
||||
test,
|
||||
} from "node:test";
|
||||
import { executeRequestGenerators } from "../../src/api/sync/generatorSupport.ts";
|
||||
import { spawnAPI } from "./api.testUtils.ts";
|
||||
|
||||
const parityFiles = {
|
||||
@@ -364,12 +366,16 @@ function runParityBatch(api: API, cases: readonly ParityCase[]): void {
|
||||
}
|
||||
|
||||
function* observeRequestBatches<Result>(
|
||||
requestGenerator: Generator<APIRequest | readonly APIRequest[], Result, any>,
|
||||
requestGenerator: Generator<any, Result, any>,
|
||||
requestBatches: string[][],
|
||||
): Generator<APIRequest | readonly APIRequest[], Result, any> {
|
||||
): Generator<any, Result, any> {
|
||||
let state = requestGenerator.next();
|
||||
while (!state.done) {
|
||||
const request = state.value;
|
||||
if (!Array.isArray(request) && "deferred" in request) {
|
||||
state = requestGenerator.next(yield request);
|
||||
continue;
|
||||
}
|
||||
const requests = Array.isArray(request) ? request : [request as APIRequest];
|
||||
requestBatches.push(requests.map(current => current.method));
|
||||
state = requestGenerator.next(yield request);
|
||||
@@ -530,33 +536,301 @@ describe("API - generator batching", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("all deduplicates only initialize requests within a batch round", () => {
|
||||
test("executes deferred generators without returning their results", () => {
|
||||
const api = spawnAPI();
|
||||
const requestBatches: string[][] = [];
|
||||
function* runConcurrentRequests() {
|
||||
yield* defer(api.parseCommandLine.gen(["--strict"]));
|
||||
yield* defer(api.readConfigFile.gen("/tsconfig.json"));
|
||||
return yield* all(
|
||||
api.parseCommandLine.gen(["--target", "esnext"]),
|
||||
api.readConfigFile.gen("/base.json"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const [[firstCommandLine, secondCommandLine, config]] = api.batch(observeRequestBatches(
|
||||
all(
|
||||
api.parseCommandLine.gen(["--strict"]),
|
||||
api.parseCommandLine.gen(["--strict"]),
|
||||
api.readConfigFile.gen("/tsconfig.json"),
|
||||
),
|
||||
requestBatches,
|
||||
));
|
||||
|
||||
const [[[commandLine, config]]] = api.batch(observeRequestBatches(all(runConcurrentRequests()), requestBatches));
|
||||
assert.equal(commandLine.options.target, 99);
|
||||
assert.deepEqual(config.config, {});
|
||||
assert.deepEqual(requestBatches, [
|
||||
["initialize"],
|
||||
["parseCommandLine", "parseCommandLine", "readConfigFile"],
|
||||
["parseCommandLine", "readConfigFile"],
|
||||
]);
|
||||
assert.equal(firstCommandLine.options.strict, true);
|
||||
assert.equal(secondCommandLine.options.strict, true);
|
||||
assert.deepEqual(config.config, {});
|
||||
|
||||
const [onlyConfig] = api.batch(
|
||||
defer(api.parseCommandLine.gen(["--strict"])),
|
||||
api.readConfigFile.gen("/tsconfig.json"),
|
||||
);
|
||||
assert.deepEqual(onlyConfig.config, {});
|
||||
}
|
||||
finally {
|
||||
api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("waits for deferred work spawned after a request", () => {
|
||||
const api = spawnAPI();
|
||||
const events: string[] = [];
|
||||
function* backgroundWork() {
|
||||
events.push("started");
|
||||
yield* api.parseCommandLine.gen(["--strict"]);
|
||||
events.push("completed");
|
||||
}
|
||||
function* foregroundWork() {
|
||||
const config = yield* api.readConfigFile.gen("/tsconfig.json");
|
||||
yield* defer(backgroundWork());
|
||||
events.push("foreground completed");
|
||||
return config;
|
||||
}
|
||||
|
||||
try {
|
||||
const [config] = api.batch(foregroundWork());
|
||||
assert.deepEqual(config.config, {});
|
||||
assert.deepEqual(events, ["started", "foreground completed", "completed"]);
|
||||
}
|
||||
finally {
|
||||
api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not block on deferred work from a nested all", () => {
|
||||
const api = spawnAPI();
|
||||
const events: string[] = [];
|
||||
function* backgroundWork() {
|
||||
yield* api.readConfigFile.gen("/base.json");
|
||||
yield* api.readConfigFile.gen("/tsconfig.json");
|
||||
yield* api.readConfigFile.gen("/base.json");
|
||||
events.push("background completed");
|
||||
}
|
||||
function* nestedWork() {
|
||||
yield* defer(backgroundWork());
|
||||
return yield* api.parseCommandLine.gen(["--strict"]);
|
||||
}
|
||||
function* foregroundWork() {
|
||||
const [commandLine] = yield* all(nestedWork());
|
||||
events.push("nested all completed");
|
||||
const config = yield* api.readConfigFile.gen("/tsconfig.json");
|
||||
events.push("foreground completed");
|
||||
return { commandLine, config };
|
||||
}
|
||||
|
||||
try {
|
||||
api.parseCommandLine([]);
|
||||
const [{ commandLine, config }] = api.batch(foregroundWork());
|
||||
assert.equal(commandLine.options.strict, true);
|
||||
assert.deepEqual(config.config, {});
|
||||
assert.deepEqual(events, ["nested all completed", "foreground completed", "background completed"]);
|
||||
}
|
||||
finally {
|
||||
api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("omits deferred results in any batch position", () => {
|
||||
const api = spawnAPI();
|
||||
let synchronousWorkCompleted = false;
|
||||
function* synchronousWork() {
|
||||
synchronousWorkCompleted = true;
|
||||
return "ignored";
|
||||
}
|
||||
|
||||
try {
|
||||
const [commandLine, config] = api.batch(
|
||||
api.parseCommandLine.gen(["--strict"]),
|
||||
defer(api.readConfigFile.gen("/base.json")),
|
||||
api.readConfigFile.gen("/tsconfig.json"),
|
||||
defer(synchronousWork()),
|
||||
);
|
||||
assert.equal(commandLine.options.strict, true);
|
||||
assert.deepEqual(config.config, {});
|
||||
assert.equal(synchronousWorkCompleted, true);
|
||||
}
|
||||
finally {
|
||||
api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("throws request errors into deferred generators", () => {
|
||||
const api = spawnAPI();
|
||||
const events: string[] = [];
|
||||
function* requestWithCleanup(): Generator<APIRequest, void, APIResponse["result"]> {
|
||||
try {
|
||||
yield { method: "unknown", params: null } as unknown as APIRequest;
|
||||
}
|
||||
catch {
|
||||
events.push("caught");
|
||||
}
|
||||
finally {
|
||||
events.push("finally");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assert.deepEqual(api.batch(defer(requestWithCleanup())), []);
|
||||
assert.deepEqual(events, ["caught", "finally"]);
|
||||
}
|
||||
finally {
|
||||
api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("batch execution deduplicates only initialize requests within a round", () => {
|
||||
const requestBatches: string[][] = [];
|
||||
function* request(method: string): Generator<APIRequest, APIResponse["result"], APIResponse["result"]> {
|
||||
return yield { method, params: null } as unknown as APIRequest;
|
||||
}
|
||||
|
||||
const results = executeRequestGenerators(
|
||||
[request("initialize"), request("initialize"), request("other"), request("other")],
|
||||
requests => {
|
||||
requestBatches.push(requests.map(request => request.method));
|
||||
return requests.map(request => ({ result: request.method }));
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(requestBatches, [["initialize", "other", "other"]]);
|
||||
assert.deepEqual(results, ["initialize", "initialize", "other", "other"]);
|
||||
});
|
||||
|
||||
test("does not execute an empty request round", () => {
|
||||
let executions = 0;
|
||||
function* completed(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
executeRequestGenerators([], () => {
|
||||
executions++;
|
||||
return [];
|
||||
}),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
executeRequestGenerators([completed("first"), completed("second")], () => {
|
||||
executions++;
|
||||
return [];
|
||||
}),
|
||||
["first", "second"],
|
||||
);
|
||||
assert.equal(executions, 0);
|
||||
});
|
||||
|
||||
test("maps deduplicated responses back into request groups", () => {
|
||||
const requestBatches: string[][] = [];
|
||||
function* groupedRequests(): Generator<readonly APIRequest[], readonly string[], readonly { result: string; }[]> {
|
||||
const responses = yield [
|
||||
{ method: "initialize", params: null },
|
||||
{ method: "other", params: null },
|
||||
{ method: "initialize", params: null },
|
||||
] as unknown as readonly APIRequest[];
|
||||
return responses.map(response => response.result);
|
||||
}
|
||||
function* initializeRequest(): Generator<APIRequest, string, string> {
|
||||
return yield { method: "initialize", params: null } as unknown as APIRequest;
|
||||
}
|
||||
|
||||
const results = executeRequestGenerators([groupedRequests(), initializeRequest()], requests => {
|
||||
requestBatches.push(requests.map(request => request.method));
|
||||
return requests.map((request, index) => ({ result: `${request.method}:${index}` }));
|
||||
});
|
||||
|
||||
assert.deepEqual(requestBatches, [["initialize", "other"]]);
|
||||
assert.deepEqual(results, [["initialize:0", "other:1", "initialize:0"], "initialize:0"]);
|
||||
});
|
||||
|
||||
test("isolates handled errors from other generators in the same round", () => {
|
||||
const requestBatches: string[][] = [];
|
||||
function* recoveringRequest(): Generator<APIRequest, string, string> {
|
||||
try {
|
||||
yield { method: "bad", params: null } as unknown as APIRequest;
|
||||
}
|
||||
catch {
|
||||
return yield { method: "recovery", params: null } as unknown as APIRequest;
|
||||
}
|
||||
return "unexpected";
|
||||
}
|
||||
function* successfulRequest(): Generator<APIRequest, string, string> {
|
||||
return yield { method: "good", params: null } as unknown as APIRequest;
|
||||
}
|
||||
|
||||
const results = executeRequestGenerators([recoveringRequest(), successfulRequest()], requests => {
|
||||
requestBatches.push(requests.map(request => request.method));
|
||||
return requests.map(request => request.method as string === "bad" ? { result: undefined, error: "failed" } : { result: request.method });
|
||||
});
|
||||
|
||||
assert.deepEqual(requestBatches, [["bad", "good"], ["recovery"]]);
|
||||
assert.deepEqual(results, ["recovery", "good"]);
|
||||
});
|
||||
|
||||
test("executes recursively deferred generators", () => {
|
||||
const events: string[] = [];
|
||||
function* grandchild() {
|
||||
yield { method: "grandchild", params: null } as unknown as APIRequest;
|
||||
events.push("grandchild completed");
|
||||
}
|
||||
function* child() {
|
||||
yield* defer(grandchild());
|
||||
yield { method: "child", params: null } as unknown as APIRequest;
|
||||
events.push("child completed");
|
||||
}
|
||||
function* parent() {
|
||||
yield* defer(child());
|
||||
yield { method: "parent", params: null } as unknown as APIRequest;
|
||||
events.push("parent completed");
|
||||
}
|
||||
|
||||
executeRequestGenerators([parent()], requests => requests.map(request => ({ result: request.method })));
|
||||
|
||||
assert.deepEqual(events, ["parent completed", "child completed", "grandchild completed"]);
|
||||
});
|
||||
|
||||
test("surfaces deferred errors after the parent completes", () => {
|
||||
const events: string[] = [];
|
||||
function* background() {
|
||||
yield { method: "background", params: null } as unknown as APIRequest;
|
||||
}
|
||||
function* parent() {
|
||||
yield* defer(background());
|
||||
events.push("parent completed");
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => executeRequestGenerators([parent()], requests => requests.map(() => ({ result: undefined, error: "deferred failure" }))),
|
||||
/deferred failure/,
|
||||
);
|
||||
assert.deepEqual(events, ["parent completed"]);
|
||||
});
|
||||
|
||||
test("appends deferred work after the parent request", () => {
|
||||
const requestBatches: string[][] = [];
|
||||
function* background() {
|
||||
yield { method: "background", params: null } as unknown as APIRequest;
|
||||
}
|
||||
function* parent() {
|
||||
yield* defer(background());
|
||||
yield { method: "parent", params: null } as unknown as APIRequest;
|
||||
}
|
||||
|
||||
executeRequestGenerators([parent()], requests => {
|
||||
requestBatches.push(requests.map(request => request.method));
|
||||
return requests.map(request => ({ result: request.method }));
|
||||
});
|
||||
|
||||
assert.deepEqual(requestBatches, [["parent", "background"]]);
|
||||
});
|
||||
|
||||
test("rejects repeated generator instances", () => {
|
||||
function* request(): Generator<APIRequest, string, string> {
|
||||
return yield { method: "request", params: null } as unknown as APIRequest;
|
||||
}
|
||||
const generator = request();
|
||||
|
||||
assert.throws(
|
||||
() => executeRequestGenerators([generator, generator], requests => requests.map(request => ({ result: request.method }))),
|
||||
/same generator instance more than once/,
|
||||
);
|
||||
});
|
||||
|
||||
test("yields source file metadata requests on cache misses", () => {
|
||||
const api = spawnAPI();
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user