I have a function (nodejs on aws Lambda) that reads all the files in a directory on s3, converting each, and merging their contents before returning to the front end.
The main function is as follows:
export async function handleFilesRequest(foundry: string, type: string): Promise<APIGatewayProxyResult> {
let accumulatingMap: Map<string, any> = new Map<string, any>();
try {
accumulatingMap = await getFileList(foundry, type)
.then((list) => {
return list.map(async key => {
readFile(key)
.then(content => {
return transformFile(content)
})
.then((currMap) => {
accumulatingMap = new Map<string, any>([...accumulatingMap.entries(), ...currMap.entries()])
return accumulatingMap;
})
.then((acc) => {
// a console.log here shows that this has the complete set of values (but not in time, I guess)
return accumulatingMap;
})
})
}).catch(e => {
return new Map<string, any>();
}) as Map<string, any>;
return OK.format(JSON.stringify(accumulatingMap));
} catch (e) {
return ServerError.format(e);
}
}
What happens is that the variable accumulatingMap
has not had time to get its values before it is returned in the line:
return OK.format(JSON.stringify(accumulatingMap));
how do I ensure that accumulatingMap has the time to complete, or is completed, before that return line is called?
I have tried variations of Promise.all and Promise.resolve, but these just seem to wrap things in further Promise’s and don’t actually halt the process in time for the Values to complete. Or, at least, it didn’t seem to work for me.
how do I make this work?
BTW: the statement from MDN web docs (The await operator is used to wait for a Promise and get its fulfillment value.) does not seem to to work, so a sub-question is: what is await
really for?
The functions that this function delegates to all appear to work (doing as I expect them to do), so I have listed them here as a reference:
const readFile = async (key: string): Promise<string> => {
if (key?.endsWith('.json')) {
const command = new GetObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: key,
});
return s3Client.send(command).then(b => {
return b.Body?.transformToString() as Promise<string>;
});
}
return ''
}
const transformFile = (content: string): Map<string, any> => {
let objFileContents = JSON.parse(content);
let fileResult: Map<string, any> = new Map<string, any>();
if (objFileContents) {
(Object.keys(objFileContents)).forEach(k => {
fileResult.set(objFileContents[k].name, objFileContents[k])
return fileResult;
})
}
return fileResult;
}
const getFileList = async (foundry: string, type: string): Promise<string[]> => {
const command = new ListObjectsCommand({
Bucket: S3_BUCKET_NAME,
Delimiter: '/',
Prefix: `resources/${foundry}/`
});
const listedObjects = [];
const data: ListObjectsCommandOutput = await s3Client.send(command);
if (data['Contents'] && data['Contents'].length > 0) {
for (let index = 0; index < data['Contents'].length; index++) {
let currentKey = data['Contents'][index]['Key'];
if (currentKey?.includes(type)) {
listedObjects.push(currentKey);
}
}
}
return listedObjects;
}
2
Answers
The problem is that you don’t wait for promises in Array::map, map returns an array, not a promise. The second issue is that you don’t return
readFile(key)
promise from the map.try to update your code with this:
The mixed usage of
await
and.then
is absolutely an anti-pattern, and almost definitely causing the code to behave differently to how you expect.I’ve rewritten the code to just use
await
, and to have a more direct structure:In general, there’s a few issues with your original code:
.then
on things that don’t return promises (e.g.transformFile
).then
on something you’ve alreadyawait
ed, which doesn’t work the way you expectI’d highly recommend reading more about the difference between async/await and the traditional .then promise API; as the control flow is quite different between the two of them.