From 7f68c04adebb307eb14748156e65e2691872ac3b Mon Sep 17 00:00:00 2001 From: Guilherme Dellagustin Date: Fri, 14 Apr 2023 00:26:57 +0200 Subject: [PATCH] Refactoring #244 - Comments robustness This commit is a refactoring on the Comments function to improve robustness for loading partial responses form the comments API. Once we introduced partial responses (i.e. chunked encoding), it worked in dev, but failed in production due to buffering in the reverse proxy (nginx). This was already solved with https://github.com/Podcastindex-org/web-ui/pull/247, but only if the reverse proxy remains unchanged. With this refactoring, the implementation would continue to work even if buffering took place, making it more robust. For testing, the change introduced in the PR mentioned above was reverted (temporarily). For additinal testing instructions, see https://github.com/Podcastindex-org/web-ui/pull/247#issuecomment-1507633512 --- server/index.js | 2 +- ui/src/components/Comments/index.tsx | 56 ++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/server/index.js b/server/index.js index 0df9b34a..767730ef 100644 --- a/server/index.js +++ b/server/index.js @@ -178,7 +178,7 @@ function writeThreadcapChunk(processedNodeId, threadcap, sentCommenters, res) { threadcapChunk.commenters[comment.attributedTo] = threadcap.commenters[comment.attributedTo]; } - res.write(JSON.stringify(threadcapChunk)) + res.write(JSON.stringify(threadcapChunk) + '\n') } // ------------------------------------------------ diff --git a/ui/src/components/Comments/index.tsx b/ui/src/components/Comments/index.tsx index e175d91b..6a8a58ea 100644 --- a/ui/src/components/Comments/index.tsx +++ b/ui/src/components/Comments/index.tsx @@ -129,18 +129,50 @@ export default class Comments extends React.PureComponent { const thisComponent = this; + let incompletePartialResponse: string = ''; + await reader.read().then(function processChunk({done, value}) { + /** + * if everything goes right, value should be singe complete JSON encoded object, + * but we have seen that a reverse proxy can mess this up. + * Thus, we introduced '\n' as a delimiter, and we assume that value could be also be + * fragments of one or more concatenated JSON objects. + * e.g.: + * '{"root":' - one incomplete JSON object + * '{...}\n' - a complete JSON object (indicated by ending in \n) + * '{...}\n{"root":' - complete JSON object followed by an incomplete JSON object + * '}\n{"root":' - an final fragment of a JSON object followed by an incomplete JSON object + */ if(done) { thisComponent.setState({ loadingComments: false }); return; } - const parsedChunk = JSON.parse(new TextDecoder().decode(value)); - - updateResponseBody(responseBody, parsedChunk); + const decodedValue = new TextDecoder().decode(value); + let partialResponses = decodedValue.split('\n'); + + if(partialResponses.length > 0) { + partialResponses[0] = incompletePartialResponse + partialResponses[0]; + + incompletePartialResponse = partialResponses[partialResponses.length-1]; + + partialResponses = partialResponses.slice(0, -1); + + // normally, at this point partialResponses.length equals 1, but + // if multiple chunks were buffered and delivered as one, it could be + // > 1 + + if(incompletePartialResponse || partialResponses.length > 1) { + console.warn('Comments: Unexpected chunked responses from the comments API - a workaround is in place to ensure functional correctness, but performance is impacted.') + } + } + + const parsedPartialResponses = partialResponses.map((partialResponse) => JSON.parse(partialResponse)); + updateResponseBody(responseBody, parsedPartialResponses); + const stateToSet: any = { showComments: true, }; @@ -151,14 +183,16 @@ export default class Comments extends React.PureComponent { return reader.read().then(processChunk); }); - function updateResponseBody(responseBody, parsedChunk) { - responseBody.roots = responseBody.roots.concat(parsedChunk.roots); - for(let key in parsedChunk.nodes) { - responseBody.nodes[key] = parsedChunk.nodes[key]; - } - for(let key in parsedChunk.commenters) { - responseBody.commenters[key] = parsedChunk.commenters[key]; - } + function updateResponseBody(responseBody, parsedPartialResponses: Array) { + parsedPartialResponses.forEach((parsedPartialResponse) => { + responseBody.roots = responseBody.roots.concat(parsedPartialResponse.roots); + for(let key in parsedPartialResponse.nodes) { + responseBody.nodes[key] = parsedPartialResponse.nodes[key]; + } + for(let key in parsedPartialResponse.commenters) { + responseBody.commenters[key] = parsedPartialResponse.commenters[key]; + } + }) } } else {