Skip to content

Commit 2c6a8ac

Browse files
committed
fix: fixed correct awaiting and spawning
1 parent b5a647b commit 2c6a8ac

File tree

11 files changed

+471
-103
lines changed

11 files changed

+471
-103
lines changed

src/__tests__/generators.test.mjs

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ok, strictEqual } from 'node:assert';
22
import { describe, it } from 'node:test';
33

44
import createGenerator from '../generators.mjs';
5-
import { isAsyncGenerator } from '../streaming.mjs';
65

76
describe('createGenerator', () => {
87
// Simple mock input for testing
@@ -37,101 +36,115 @@ describe('createGenerator', () => {
3736
it('should return the ast input directly when generators list is empty', async () => {
3837
const { runGenerators } = createGenerator(mockInput);
3938

40-
const result = await runGenerators({
39+
const results = await runGenerators({
4140
...mockOptions,
4241
generators: ['ast'],
4342
});
4443

45-
// The 'ast' key should resolve to the original input
46-
ok(result);
44+
// Returns array of results, first element is the 'ast' result
45+
ok(Array.isArray(results));
46+
strictEqual(results.length, 1);
47+
ok(results[0]);
4748
});
4849

4950
it('should run metadata generator', async () => {
5051
const { runGenerators } = createGenerator(mockInput);
5152

52-
const result = await runGenerators({
53+
const results = await runGenerators({
5354
...mockOptions,
5455
generators: ['metadata'],
5556
});
5657

57-
// metadata returns an async generator
58-
ok(isAsyncGenerator(result));
58+
// Returns array with one element - the collected metadata array
59+
ok(Array.isArray(results));
60+
strictEqual(results.length, 1);
61+
ok(Array.isArray(results[0]));
5962
});
6063

6164
it('should handle generator with dependency', async () => {
6265
const { runGenerators } = createGenerator(mockInput);
6366

6467
// legacy-html depends on metadata
65-
const result = await runGenerators({
68+
const results = await runGenerators({
6669
...mockOptions,
6770
generators: ['legacy-html'],
6871
});
6972

70-
// Should complete without error
71-
ok(result !== undefined);
73+
// Should complete without error - returns array of results
74+
ok(Array.isArray(results));
75+
strictEqual(results.length, 1);
7276
});
7377

7478
it('should skip already scheduled generators', async () => {
7579
const { runGenerators } = createGenerator(mockInput);
7680

7781
// Running with ['metadata', 'metadata'] should skip the second
78-
const result = await runGenerators({
82+
const results = await runGenerators({
7983
...mockOptions,
8084
generators: ['metadata', 'metadata'],
8185
});
8286

83-
ok(isAsyncGenerator(result));
87+
// Returns array with two elements (same result cached for both)
88+
ok(Array.isArray(results));
89+
strictEqual(results.length, 2);
8490
});
8591

8692
it('should handle multiple generators in sequence', async () => {
8793
const { runGenerators } = createGenerator(mockInput);
8894

89-
// Run metadata twice - the system should skip the already scheduled one
90-
// Avoid json-simple since it writes to disk
91-
const result = await runGenerators({
95+
// Run metadata - just one generator
96+
const results = await runGenerators({
9297
...mockOptions,
9398
generators: ['metadata'],
9499
});
95100

96-
// Result should be from the last generator
97-
ok(result !== undefined);
101+
// Returns array of results
102+
ok(Array.isArray(results));
103+
strictEqual(results.length, 1);
98104
});
99105

100106
it('should collect async generator results for dependents', async () => {
101107
const { runGenerators } = createGenerator(mockInput);
102108

103109
// legacy-json depends on metadata (async generator)
104-
const result = await runGenerators({
110+
const results = await runGenerators({
105111
...mockOptions,
106112
generators: ['legacy-json'],
107113
});
108114

109-
ok(result !== undefined);
115+
ok(Array.isArray(results));
116+
strictEqual(results.length, 1);
110117
});
111118

112119
it('should use multiple threads when specified', async () => {
113120
const { runGenerators } = createGenerator(mockInput);
114121

115-
const result = await runGenerators({
122+
const results = await runGenerators({
116123
...mockOptions,
117124
threads: 4,
118125
generators: ['metadata'],
119126
});
120127

121-
ok(isAsyncGenerator(result));
128+
// Returns array of results
129+
ok(Array.isArray(results));
130+
strictEqual(results.length, 1);
131+
ok(Array.isArray(results[0]));
122132
});
123133

124134
it('should pass options to generators', async () => {
125135
const { runGenerators } = createGenerator(mockInput);
126136

127137
const customTypeMap = { TestType: 'https://example.com/TestType' };
128138

129-
const result = await runGenerators({
139+
const results = await runGenerators({
130140
...mockOptions,
131141
typeMap: customTypeMap,
132142
generators: ['metadata'],
133143
});
134144

135-
ok(isAsyncGenerator(result));
145+
// Returns array of results
146+
ok(Array.isArray(results));
147+
strictEqual(results.length, 1);
148+
ok(Array.isArray(results[0]));
136149
});
137150
});

src/generators.mjs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const createGenerator = input => {
120120
* their dependencies to complete.
121121
*
122122
* @param {GeneratorOptions} options - Generator runtime options
123-
* @returns {Promise<unknown>} Result of the last generator in the pipeline
123+
* @returns {Promise<unknown[]>} Results of all requested generators
124124
*/
125125
const runGenerators = async options => {
126126
const { generators, threads } = options;
@@ -138,15 +138,31 @@ const createGenerator = input => {
138138
// Schedule all generators using the shared pool
139139
scheduleGenerators(options, sharedPool);
140140

141-
// Wait for the last generator's result
142-
const result = await cachedGenerators[generators[generators.length - 1]];
141+
// Wait for ALL requested generators to complete (not just the last one)
142+
const results = [];
143143

144-
// Terminate workers after all work is complete
145-
await sharedPool.terminate();
144+
for (const generatorName of generators) {
145+
let result = await cachedGenerators[generatorName];
146+
147+
// If the generator returns an async generator, consume it
148+
// to ensure all side effects (file writes, etc.) complete
149+
if (isAsyncGenerator(result)) {
150+
generatorsLogger.debug(
151+
`Consuming async generator output from "${generatorName}"`
152+
);
153+
154+
result = await streamingCache.getOrCollect(generatorName, result);
155+
}
156+
157+
results.push(result);
158+
}
159+
160+
// Terminate workers after all work is complete (fire-and-forget)
161+
sharedPool.terminate();
146162

147163
sharedPool = null;
148164

149-
return result;
165+
return results;
150166
};
151167

152168
return { runGenerators };

src/generators/metadata/index.mjs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,18 @@ export default {
2727
* @param {Partial<Omit<GeneratorOptions, 'worker'>>} options - Serializable options
2828
* @returns {Promise<ApiDocMetadataEntry[]>} Metadata entries for processed files
2929
*/
30-
async processChunk(fullInput, itemIndices, { typeMap }) {
31-
const results = [];
32-
33-
for (const idx of itemIndices) {
34-
results.push(...parseApiDoc(fullInput[idx], typeMap));
35-
}
36-
37-
return results;
38-
},
30+
processChunk: Object.assign(
31+
async (fullInput, itemIndices, { typeMap }) => {
32+
const results = [];
33+
34+
for (const idx of itemIndices) {
35+
results.push(...parseApiDoc(fullInput[idx], typeMap));
36+
}
37+
38+
return results;
39+
},
40+
{ sliceInput: true } // Only needs individual items, not full context
41+
),
3942

4043
/**
4144
* @param {Input} inputs

src/generators/types.d.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,20 @@ declare global {
131131
* @param options - Generator options (without worker, which isn't serializable)
132132
* @returns Array of results for the processed items
133133
*/
134-
processChunk?: (
134+
processChunk?: ((
135135
fullInput: I,
136136
itemIndices: number[],
137137
options: Partial<Omit<GeneratorOptions, 'worker'>>
138-
) => Promise<unknown[]>;
138+
) => Promise<unknown[]>) & {
139+
/**
140+
* When true, only the items at the specified indices are sent to workers
141+
* instead of the full input array. This reduces serialization overhead
142+
* for generators that don't need full context to process individual items.
143+
*
144+
* Set this to true when processChunk only accesses `fullInput[idx]` for
145+
* each index in itemIndices, and doesn't need the full array for context.
146+
*/
147+
sliceInput?: boolean;
148+
};
139149
}
140150
}

src/generators/web/index.mjs

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { processJSXEntries } from './utils/processing.mjs';
1313
* - Client-side JavaScript with code splitting
1414
* - Bundled CSS styles
1515
*
16+
* Note: This generator does NOT support streaming/chunked processing because
17+
* processJSXEntries needs all entries together to generate code-split bundles.
18+
*
1619
* @type {GeneratorMetadata<Input, string>}
1720
*/
1821
export default {
@@ -21,69 +24,38 @@ export default {
2124
description: 'Generates HTML/CSS/JS bundles from JSX AST entries',
2225
dependsOn: 'jsx-ast',
2326

24-
/**
25-
* Process a chunk of JSX AST entries.
26-
* This simply passes through the entries for aggregation in the main generate function.
27-
* The actual processing happens in processJSXEntries which needs all entries together.
28-
*
29-
* @param {import('../jsx-ast/utils/buildContent.mjs').JSXContent[]} fullInput
30-
* @param {number[]} itemIndices
31-
*/
32-
processChunk(fullInput, itemIndices) {
33-
const results = [];
34-
35-
for (const idx of itemIndices) {
36-
results.push(fullInput[idx]);
37-
}
38-
39-
return results;
40-
},
41-
4227
/**
4328
* Main generation function that processes JSX AST entries into web bundles.
4429
*
4530
* @param {import('../jsx-ast/utils/buildContent.mjs').JSXContent[]} entries - JSX AST entries to process.
4631
* @param {Partial<GeneratorOptions>} options - Generator options.
4732
* @param {string} [options.output] - Output directory for generated files.
4833
* @param {string} options.version - Documentation version string.
49-
* @returns {AsyncGenerator<Array<import('../jsx-ast/utils/buildContent.mjs').JSXContent>>}
34+
* @returns {Promise<import('../jsx-ast/utils/buildContent.mjs').JSXContent[]>}
5035
*/
51-
async *generate(entries, { output, version, worker }) {
52-
// Start loading template while chunks stream in (parallel I/O)
53-
const templatePromise = readFile(
36+
async generate(entries, { output, version }) {
37+
const template = await readFile(
5438
new URL('template.html', import.meta.url),
5539
'utf-8'
5640
);
5741

58-
// Collect all chunks as they stream in from jsx-ast
59-
const allEntries = [];
42+
// Create AST builders for server and client programs
43+
const astBuilders = createASTBuilder();
6044

61-
for await (const chunkResult of worker.stream(entries, entries, {})) {
62-
allEntries.push(...chunkResult);
45+
// Create require function for resolving external packages in server code
46+
const requireFn = createRequire(import.meta.url);
6347

64-
yield chunkResult;
65-
}
48+
// Process all entries: convert JSX to HTML/CSS/JS
49+
const { results, css, chunks } = await processJSXEntries(
50+
entries,
51+
template,
52+
astBuilders,
53+
requireFn,
54+
{ version }
55+
);
6656

67-
// Now that all chunks are collected, process them together
68-
// (processJSXEntries needs all entries to generate code-split bundles)
57+
// Process all entries together (required for code-split bundles)
6958
if (output) {
70-
const template = await templatePromise;
71-
72-
// Create AST builders for server and client programs
73-
const astBuilders = createASTBuilder();
74-
75-
// Create require function for resolving external packages in server code
76-
const requireFn = createRequire(import.meta.url);
77-
78-
// Process all entries: convert JSX to HTML/CSS/JS
79-
const { results, css, chunks } = await processJSXEntries(
80-
allEntries,
81-
template,
82-
astBuilders,
83-
requireFn,
84-
{ version }
85-
);
86-
8759
// Write HTML files
8860
for (const { html, api } of results) {
8961
await writeFile(join(output, `${api}.html`), html, 'utf-8');
@@ -97,5 +69,7 @@ export default {
9769
// Write CSS bundle
9870
await writeFile(join(output, 'styles.css'), css, 'utf-8');
9971
}
72+
73+
return results;
10074
},
10175
};

0 commit comments

Comments
 (0)