Hi Matt,
There are really two design options here:
1. Have RequestCallback::done() not return until the whole reply has been streamed. In this case, you can have the RequestCallback::done() implementation invoke the recursive loop to stream data back to the client. This is also nice in that if an exception is thrown in the process, it'll propagate back to the client.
2. Have RequestCallback::done() return immediately, and then the server streams to the client asynchronously. In this case, the RequestCallback::done() implementation would still *start* the promise loop to stream the reply, but it would save the kj::Promise<void> off to the side (e.g. in a member variable) instead of returning it. As long as that promise stays alive, it'll keep running. The client will need to make sure not to destroy the RequestCallback::Client until all the reply data has been received, otherwise it might inadvertently cancel the reply stream.
A major problem with #2 is that exceptions don't go anywhere. If an exception is thrown, the server just stops streaming. The Promise goes into the rejected state, but since no one is waiting for it, no one ever learns about the exception. You *should* call .eagerlyEvaluate(errorHandler) on the promise to provide an error handler function that at least logs the error or something. But, that's still not great, since ideally the error would propagate to the client somehow. Hence I recommend design #1 instead.
Note that in either design, you could consider having the client pass ReplyCallback as a parameter to RequestCallback::done(), rather than to reqReply(). This works since the server doesn't start responding until the client has sent its whole request stream, as you said. This is mostly an aesthetic design choice but sometimes one way or the other might be easier to deal with in the code.
-Kenton