Skip to content

proposal: preserve fetching order#736

Closed
flochtililoch wants to merge 1 commit intomasterfrom
florent/sequenced-requests
Closed

proposal: preserve fetching order#736
flochtililoch wants to merge 1 commit intomasterfrom
florent/sequenced-requests

Conversation

@flochtililoch
Copy link
Collaborator

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 a 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.

To be discussed:

  • naming: is "preserve-order" descriptive enough? should we use a boolean or an enum?
  • visibility: should we make this feature public / documented or keep it "dark" for now?
  • scope: should we think of this capability to be applied to other, non-fetching behaviors?

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.
@palaparthi
Copy link
Contributor

naming: is "preserve-order" descriptive enough?

"cancel-stale-requests" or "discard-stale-requests" might be more descriptive?

@adamstep
Copy link
Contributor

adamstep commented Nov 1, 2023

I was curious how this is implemented in htmx. Looks like that library stores a queue of requests, and using an attribute there are different strategies for managing the queue if the element issues multiple requests. See the docs here: https://htmx.org/attributes/hx-sync/

Do you think a similar approach could work in Hyperview?

@adamstep
Copy link
Contributor

adamstep commented Nov 3, 2023

Source code from htmx to deal with this: https://github.com/bigskysoftware/htmx/blob/master/src/htmx.js#L2980

@palaparthi palaparthi removed their request for review February 20, 2024 21:17
@adamstep
Copy link
Contributor

Looking at the htmx implementation a bit more, I think perhaps we could support this as follows:

Introduce two new optional behavior attributes:

  • sync-id: An ID that will be used to synchronize requests. Could be the same as the ID of the element triggering the behavior, but doesn't need to be
  • sync-method: drop (default), replace
    • drop: if there's a request in flight associated with the sync ID, drop this request
    • replace: if there's a request in flight associated with the sync ID, abort the in-flight request and make the new request

Over time, we can add queuing support as well if we think it's necessary.

Implementation:

  • Capture the two new attributes and pass them to Parser.loadElement.
  • In Parser, create an object that maps the sync IDs to the current request (promise).
  • In loadElement:
    • if NO SYNC ID: proceed as before, make request
    • if HAS SYNC ID: look up the value in the map for the ID
      • if REQ IN FLIGHT FOR SYNC ID: check the sync method. If drop, do a no-op. If replace, make the request and update the promise in the map
      • if NO REQ IN FLIGHT FOR SYNC ID: make the request and update the promise in the map
    • When we get a response:
      • if NO SYNC ID: proceed as before, return new doc
      • if HAS SYNC ID: look up value in the map for the ID. If method is replace and the promise in the map is different, do a no-op (we've been replaced with a newer request).

We will need to update the return type of loadElement so that it can either contain a doc/headerType or a no-op constant, indicating to the caller that no change should happen.

adamstep added a commit that referenced this pull request Aug 15, 2025
…aviors (#1243)

Adding request synchronization as [described
here](#736 (comment)).
This will help prevent issues where responses can complete out-of-order
from the requests.

Synchronization works on behaviors that make HTTP requests. Support is
added through two behaviors:
- `sync-id`: An ID used to synchronize requests. All requests with this
ID will be subject to the sync method.
- `sync-method`. One of `drop` (default) or `replace`:
- if `drop`, subsequent requests with the same sync ID will not be made
if there is a request currently in-flight
- if `replace`, subsequent requests will be made. However, only the
response of the last will be processed, the other responses will be
dropped and treated as a no-op.

### Implementation
The fetch API in React Native does not support cancellation with a
signal. So instead, we need to track the current request for a given
sync ID, and ignore responses from other requests (depending on sync
method).

- Add support for extracting and passing along the `sync-id` and
`sync-method` attributes.
- In `parser.ts`, update the logic that makes requests to support
optional synchronization.
- If `syncId` is provided, store request promises in a map, keyed by
`syncId`
- Logic based on `syncMethod` is implemented around the fetch logic.
- `drop` method is processed before the request is made, potentially
returning a `NO_OP` if there is another request in flight for `syncId`.
- `replace` method is processed when the response is received,
potentially returning a `NO_OP` if there's a newer request in flight for
`syncId`.


### Other changes
- [x] Updated demo with new "sync" screen that shows the `replace`
method in action. Uses a dynamic response that delays responses by a
random amount, to stress test the sync capabilities.
- [x] Updated schema with the new attributes.
- [ ] Update website docs
@adamstep
Copy link
Contributor

Superceded with syncing implementation

@adamstep adamstep closed this Sep 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants