whatwg / fetch

Fetch Standard
https://fetch.spec.whatwg.org/
Other
2.12k stars 333 forks source link

Pause readable stream reader #1651

Closed osintalex closed 1 year ago

osintalex commented 1 year ago

As far as I know, once you've started reading a stream from response.body you can't pause and resume it like you can with node.

I think this would be very useful. I've tested this with the below React code sending back a stream of JSON from an app I'm running locally. I thought that if I used sleep in a blocking way it would stop the reader from reading but I noticed that it keeps reading in the background.

Perhaps I've made a mistake here and there is a way to achieve this in pure JS, I'd be really grateful for an example of that if so!

import logo from "./logo.svg";
import "./App.css";

function App() {
  let pause = false;
  let cancel = false;
  function changePause() {
    pause = !pause;
  }
  function cancelStream() {
    cancel = true;
  }
  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async function* iterateOverStream(stream) {
    const reader = stream.getReader();
    const decoder = new TextDecoder();

    while (true) {

      if (pause) {
        // Slow down the infinite loop repeating until pause is pressed again
        // I thought that this would pause the reader from reading but it doesn't
        // it just pauses displaying the data, the reader keeps reading in the background
        await sleep( 3 * 1000);
        continue;
      } else {
        const { done, value } = await reader.read();
        if (done) break;
        if (cancel) {
          reader.cancel();
          break;
        }
        yield decoder.decode(value, { stream: !done });
      }
    }
    reader.releaseLock();
  }

  async function readData() {
    cancel = false;
    const response = await fetch("http://localhost:5000");
    for await (const chunk of iterateOverStream(response.body)) {
      console.log(`json chunk is ${chunk}`);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={readData}>
          Click me and check out dev tools to test stream
        </p>
        <p onClick={changePause}>Click to pause/unpause</p>
        <p onClick={cancelStream}>Click to cancel stream</p>
      </header>
    </div>
  );
}

export default App;
annevk commented 1 year ago

cc @saschanaz @MattiasBuelens

saschanaz commented 1 year ago

Do you have the full code including the server part? Looks like it should stop reading at some point, could be a browser bug.

osintalex commented 1 year ago

Hey! Thanks for the reply - sever code is below. It's a very simple Flask app in Python that gives results like this:

{
    "items": [
        {
            "hello": "world"
        },
        ...
        {
            "hello": "world"
        }
    ]
}

To be clear, it does stop reading when it's reached the 2000th item. But I'm looking for a way to pause and resume it any arbitrary point in the stream.

import json
from flask import Flask, Response
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route('/')
def home():

    def my_generator():
        yield '{"items": ['
        for count in range(1, 2001):
            my_json_object = json.dumps({"hello": "world"})
            if count == 2000:
                yield my_json_object
            else:
                yield my_json_object + ", "
        yield ']}'
    return Response(my_generator(), status=200, content_type='application/json')

if __name__ == '__main__':
    app.run(debug=True)
saschanaz commented 1 year ago

Looks like all browser engines immediately fetch every byte from fetch("http://localhost:5000") even without accessing .body at all. And I'm not familiar with how the fetch suspension works exactly. @jesup, can you help?

(I tweaked the number to 200001 and 200000 and browsers still fetched all 40 MB data)

ricea commented 1 year ago

I believe the reason for continuing to read from the network even when JavaScript has stopped reading is to ensure that the HTTP cache is populated.

Try adding a Cache-Control: no-store header to your response. This avoids writing to the HTTP cache, and so should enable reading the response to be paused.

saschanaz commented 1 year ago
r = Response(my_generator(), status=200, content_type='application/json')
r.cache_control.no_cache = True
return r

This adds the header but that still doesn't seem to suspend the fetch on any browser. Interesting...

saschanaz commented 1 year ago

Forget my previous comment, it's no-store and this works:

r = Response(my_generator(), status=200, content_type='application/json')
r.cache_control.no_store = True
return r

Hope this helps!

osintalex commented 1 year ago

Thanks so much everyone! Really helpful responses, I would never have figured this out on my own.