Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from "./generatorSupport.ts";
// import {executeRequestGenerators, type ExecutedGeneratorsResults, type APIRequestGenerator} from "./generatorSupport.ts";
// @sync-only-end

export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHost {
Expand Down Expand Up @@ -260,12 +260,7 @@ 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;
// return executeRequestGenerators(requestGenerators, requests => this.client.batchRequests(requests).responses);
// }
// @sync-only-end

Expand Down
11 changes: 3 additions & 8 deletions packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,11 @@ export interface TranspileOutput {
sourceMapText?: string;
}

export { all } from "./generatorSupport.ts";
export { all, defer } from "./generatorSupport.ts";
import {
all,
type APIRequestGenerator,

@dragomirtitian Titian Cernicova-Dragomir (dragomirtitian) Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we expose UndeferredAPIRequestGenerator and APIRequestGenerator. If the generators are recursive they need a type. We could redefine some of these types on the usage side but they are now more complicated that a simple generator.

If we expose it, I'm not sure UndeferredAPIRequestGenerator is the best name for the common case of the generator, and this is the type that is needed as the return type since APIRequestGenerator is a union and does not work well as a generator return type:

function *testNotOk(n: number): sync.APIRequestGenerator<number> {
    if(n === 0) return 0;
    return yield * testNotOk(n-1)
}


function *testOk(n: number): sync.UndeferredAPIRequestGenerator<number> {
    if(n === 0) return 0;
    return yield * testOk(n-1)
}

type ExecutedGeneratorsResults,
executeRequestGenerators,
} from "./generatorSupport.ts";

export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHost {
Expand Down Expand Up @@ -288,12 +288,7 @@ 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;
return executeRequestGenerators(requestGenerators, requests => this.client.batchRequests(requests).responses);
}

private get ensureInitialized(): {
Expand Down
185 changes: 128 additions & 57 deletions packages/typescript/src/api/sync/generatorSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,82 +22,145 @@ 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: UndeferredAPIRequestGenerator;
}
type APIRequestGeneratorYield = APIRequest | readonly APIRequest[] | DeferredAPIRequest;
export type UndeferredAPIRequestGenerator<Return = any> = Generator<APIRequestGeneratorYield, Return, any>;
export type DeferredAPIRequestGenerator = Generator<DeferredAPIRequest, void, unknown> & { readonly [deferredGeneratorMarker]: true; };
export type APIRequestGenerator<Return = any> = UndeferredAPIRequestGenerator<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 APIRequestGenerator[]> = number extends T["length"] ? GeneratorReturn<Exclude<T[number], DeferredAPIRequestGenerator>>[]
: T extends readonly [infer Head extends APIRequestGenerator, ...infer Tail extends readonly APIRequestGenerator[]] ? 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;
getDeduplicationKey?: (request: APIRequest) => string | undefined;
}

function createRequestRunner<T extends readonly APIRequestGenerator[]>(requestGenerators: T, options: RequestRunnerOptions = {}) {
const registeredGenerators = new Set<APIRequestGenerator>();
const requestsByGenerator = new Map<APIRequestGenerator, APIRequestGeneratorYield>();
const resultsByGenerator = new Map<APIRequestGenerator, unknown>();
for (const generator of requestGenerators) {
addGenerator(generator);
}
const requestRounds = runRequestRounds();
return { requestRounds, getResults };

function advanceGenerator(generator: APIRequestGenerator, 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: APIRequestGenerator): 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;

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);
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);
}
}
else {
state = requestGenerator.next(responseIndex.map(index => responses[index]));
if (!requestsByGenerator.size) break;

const requests: APIRequest[] = [];
const responseIndexByDeduplicationKey = new Map<string, number>();
const addRequest = (request: APIRequest): number => {
const deduplicationKey = options.getDeduplicationKey?.(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));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but... nah. registeredGenerators is ever-growing when deferrals can add new generators to the list each turn, so this is just required - we could track only unfinished ones and finished ones seperately, yes but then we're potentially allocating two new arrays per turn instead of allocating one and rarely growing one. Like the comment says, best would be iterator filter in the future to skip the filtered collection alloc entirely. 🤷‍♂️

Weak set for duplicate detection is like.... sure? But also no? Technically correct but accomplishes very little in practice - GC pressure of generator objects shouldn't be a real concern for any real code.

const responseIndices = new Map<APIRequestGenerator, 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));
}
if (state.done) {
results[i] = state.value;
completedIndices.add(i);
requestObjects[i] = undefined;

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]));
}
}
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 APIRequestGenerator[]>(
...requestGenerators: T
): UndeferredAPIRequestGenerator<ExecutedGeneratorsResults<T>>;
export function* all<T extends readonly APIRequestGenerator[]>(
...requestGenerators: T
): UndeferredAPIRequestGenerator<ExecutedGeneratorsResults<T>> {
const { requestRounds, getResults } = createRequestRunner(requestGenerators);
yield* requestRounds;
return getResults();
}

export function executeRequestGenerators<T extends readonly APIRequestGenerator[]>(
requestGenerators: T,
executeRequests: (requests: APIRequest[]) => readonly GeneratorResponse[],
): ExecutedGeneratorsResults<T> {
const { requestRounds, getResults } = createRequestRunner(requestGenerators, { executeDeferred: true, getDeduplicationKey: getRequestDeduplicationKey });
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: APIRequestGenerator): generator is DeferredAPIRequestGenerator {
return deferredGeneratorMarker in generator;
}

function isRequestGroup(request: APIRequest | readonly APIRequest[]): request is readonly APIRequest[] {
Expand All @@ -112,3 +175,11 @@ function getRequestDeduplicationKey(request: APIRequest): string | undefined {
return undefined;
}
}

export function defer(gen: UndeferredAPIRequestGenerator): DeferredAPIRequestGenerator {
const deferred = (function* (): Generator<DeferredAPIRequest, void, unknown> {
yield { deferred: gen };
})() as DeferredAPIRequestGenerator;
Object.defineProperty(deferred, deferredGeneratorMarker, { value: true });
return deferred;
}
Loading