From 46adfcd69d442ad2787eac0f8c45eb8b16535457 Mon Sep 17 00:00:00 2001 From: flochtililoch Date: Wed, 1 Nov 2023 09:21:21 +0100 Subject: [PATCH] proposal: preserve fetching order This PR introduce a new behavior boolean attribute, `preserve-order`. This attribute is used by behaviors that "fetch" content, i.e. anytime there's a `href` attribute set, with an update action such as `replace`, `replace-inner`, etc. When present, we compute an unique id before the request is emitted, and store it on an attribute of the target element. When the response arrives, we read the attribute of the target element and compare it to the one we generated before the request was sent. If the attribute is set with a value that's different from the one set before the request, we assume this means another request/response cycle took place, and the response we're presently dealing with is "outdated", so we discard it. --- examples/case_studies/contacts/index.xml.js | 4 +- examples/case_studies/contacts/list.xml.njk | 2 + src/core/components/hv-screen/index.js | 59 +++++++++++++++------ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/examples/case_studies/contacts/index.xml.js b/examples/case_studies/contacts/index.xml.js index 0198e37f3..9fa489f81 100644 --- a/examples/case_studies/contacts/index.xml.js +++ b/examples/case_studies/contacts/index.xml.js @@ -11,7 +11,9 @@ module.exports = function handler(req, res, next) { const { query } = urlParse(req.originalUrl, true); // no search or next page? pass through to 11ty to render the entire document - if (query.search === undefined && !query.page) { + // note: we also test for "template" to work mitigate this bug + // https://github.com/Instawork/hyperview/issues/735 + if (query.search === undefined && !query.page && !query.template) { next(); return; } diff --git a/examples/case_studies/contacts/list.xml.njk b/examples/case_studies/contacts/list.xml.njk index 865905f16..db5b3eeda 100644 --- a/examples/case_studies/contacts/list.xml.njk +++ b/examples/case_studies/contacts/list.xml.njk @@ -37,6 +37,8 @@ action="replace" target="contacts" href="?template=list" + debounce="200" + preserve-order="true" show-during-load="loading-indicator" /> diff --git a/src/core/components/hv-screen/index.js b/src/core/components/hv-screen/index.js index 5b92b8c64..19e9718eb 100644 --- a/src/core/components/hv-screen/index.js +++ b/src/core/components/hv-screen/index.js @@ -505,27 +505,52 @@ export default class HvScreen extends React.Component { doc: newRoot, }); + // If a target is specified and exists, use it. Otherwise, the action target defaults + // to the element triggering the action. + const getTargetElement = () => { + let targetElement = targetId ? this.doc?.getElementById(targetId) : currentElement; + if (!targetElement) { + targetElement = currentElement; + } + return targetElement + } + + // Before fetching, check if the behavior needs order to be preserved. + // If so, we need to set a flag on the element that we check upon receiving the response. + // If that flag has changed, that means another more recent request has been made, and we + // need to discard the response. + const targetElement = getTargetElement(); + const preserveOrderId = ( + behaviorElement || currentElement + )?.getAttribute('preserve-order') === 'true' + ? String(Math.random()) + : null; + if (preserveOrderId !== null) { + targetElement.setAttribute('_preserve-order-id', preserveOrderId); + } + // Fetch the resource, then perform the action on the target and undo indicators. const fetchAndUpdate = () => this.fetchElement(href, verb, newRoot, formData) .then((newElement) => { - // If a target is specified and exists, use it. Otherwise, the action target defaults - // to the element triggering the action. - let targetElement = targetId ? this.doc?.getElementById(targetId) : currentElement; - if (!targetElement) { - targetElement = currentElement; - } - - if (newElement) { - newRoot = Behaviors.performUpdate(action, targetElement, newElement); - } else { - // When fetch fails, make sure to get the latest version of the doc to avoid any race conditions - newRoot = this.doc; + const targetElement = getTargetElement(); + const shouldSkipUpdate = preserveOrderId !== null && targetElement.getAttribute('_preserve-order-id') !== preserveOrderId; + if (!shouldSkipUpdate) { + if (newElement) { + newRoot = Behaviors.performUpdate(action, targetElement, newElement); + if (preserveOrderId) { + // Un-set the preserve order id on the target element as the update is now done + targetElement.setAttribute('_preserve-order-id', ""); + } + } else { + // When fetch fails, make sure to get the latest version of the doc to avoid any race conditions + newRoot = this.doc; + } + newRoot = Behaviors.setIndicatorsAfterLoad(showIndicatorIdList, hideIndicatorIdList, newRoot); + // Re-render the modifications + this.setState({ + doc: newRoot, + }); } - newRoot = Behaviors.setIndicatorsAfterLoad(showIndicatorIdList, hideIndicatorIdList, newRoot); - // Re-render the modifications - this.setState({ - doc: newRoot, - }); if (typeof onEnd === 'function') { onEnd();