Add defer functionality to generator executor (#64157)

This commit is contained in:
Wesley Wigham authored and GitHub committed 2026-09-08 20:53:13 +00:00
1 parent ee72eb0e48
commit 10404f71a8
4 files changed
+427 -93

No files matched your search

+4 -9
View File
@@ -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
+5 -10
View File
@@ -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 {