Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP add stream support #2479

Merged
merged 16 commits into from
Mar 8, 2025
Merged

HTTP add stream support #2479

merged 16 commits into from
Mar 8, 2025

Conversation

adrieljss
Copy link
Contributor

@adrieljss adrieljss commented Mar 2, 2025

This PR was meant to add stream support for fetch.

What was changed:

Rust:

  • Removed fetch_read_body, because we're already sending response body chunks through the IPC.
  • Using reqwest::chunk to send an array of bytes (Vec<u8>) to JavaScript using the IPC Channel

JavaScript:

  • return a ReadableStream as InitBody in Response, this should be automatically collected and function as normal even if not used for streaming responses.

Example Usage

const res = await fetch('...')
const rd = res.body.getReader()

while (true) {
  const { done, value } = await rd.read()
  if (done) break
  const txt = new TextDecoder().decode(value)
  console.log(txt)
}

or (TypeScript might not like this)

const res = await fetch('...')

for await (const chunk of res.body) {
  const txt = new TextDecoder().decode(chunk)
  console.log(txt)
}

closes #2140

@adrieljss adrieljss requested a review from a team as a code owner March 2, 2025 14:26
@FabianLars
Copy link
Member

Thanks for opening a PR! Tiny bit more context for reviewers: https://discord.com/channels/616186924390023171/1344666371316908063 This is supposed to be an alternative to #2140

@adrieljss Can you please revert the changes to Cargo.toml, Cargo.lock, package.json, and pnpm-lock.yaml ? Versioning is handled in CI and this would break stuff.

@FabianLars FabianLars requested a review from amrbashir March 2, 2025 14:40
Copy link
Contributor

github-actions bot commented Mar 2, 2025

Package Changes Through 194fdee

There are 4 changes which include http with minor, http-js with minor, log with minor, log-js with minor

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
api-example 2.0.19 2.0.20
api-example-js 2.0.15 2.0.16
http 2.3.0 2.4.0
http-js 2.3.0 2.4.0
log 2.2.3 2.3.0
log-js 2.2.3 2.3.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

Comment on lines 132 to 137
#[derive(Clone, Serialize)]
pub struct StreamMessage {
value: Option<Vec<u8>>,
done: bool,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not performant because of serialization overhead.

To avoid this, you can send a tauri::Response which is just a Vec<u8> and then you could append a single byte at the end that is either 1 or 0, indicating the status of done. Or even an empty array would suffice.

I am not sure if the channel is as performant as invoke (will have to check that later) but would be better to avoid using the channel.

Copy link
Contributor Author

@adrieljss adrieljss Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're right, I will change the struct, how about using serde_bytes? Since serde could be serializing the Vec as an array of numbers.

I don't think there's any way to do streaming using invoke(?) so maybe a solution would be having a seperate function for seperate usecases, one for streaming and one for collecting all the contents at the end, but the user has to specify that and that will not be fetch-like.

Copy link
Contributor Author

@adrieljss adrieljss Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though, if the server doesn't want to stream, the channel will only receive 1 chunk and that should be (around) the same performance since both uses IPC anyways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be done with invoke and an AsyncIterator in JS:

  • The removed fetch_read_body instead of using .bytes() it will use .chunk(), effictively returning a single chunk each time it is called
  • On JS side, you can create an AsyncIterator that will be used to create the ReadableStream, on each iteration it will call fetch_read_body and append the data to the stream

Copy link
Contributor Author

@adrieljss adrieljss Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that be better though? In channels, it said that is the recommended way for streaming data (like HTTP streaming, which is our case) to the frontend. I think the invoke approach will block operations until it has returned something. While channels is just an event listener.

In my opinion, I feel like it's more intuitive rather than spamming invokes

Copy link
Member

@amrbashir amrbashir Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The channel calls invoke under the hood. Channels are more ergonomic but there is room for improvement in their implementation as it uses tauri::State with a Mutex unlike what I am proposing which would use a Mutex only until the request is sent, after that the body could be read without any Mutexes.

For now, let's use the channel approach and I can improve on it later (once I confirm that there is mutex overhead involved).

My main blocker for this PR is the use of serialization which we can avoid even with channels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes okay, I will fix that 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amrbashir can you check the new commit I did? I can't use tauri::Response inside the channel because it doesn't impl Clone and the compiler doesn't allow me to. I used serde_bytes to try and make it as efficient as possible, instead of just sending an array of numbers.

Copy link
Member

@amrbashir amrbashir Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That clone bound shouldn't be needed, I will open a PR to fix it later.
Edit: here it is tauri-apps/tauri#12876

For now, you can use InvokeResponseBody::Raw which is Clone and implements IpcResponse trait and so can be used with channels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That clone bound shouldn't be needed, I will open a PR to fix it later.

For now, you can use InvokeResponseBody::Raw which is Clone and implements IpcResponse trait and so can be used with channels.

done! 👍

amrbashir added a commit to tauri-apps/tauri that referenced this pull request Mar 3, 2025
Comment on lines 26 to 27
// reqwest::Response is never read, but might be needed for future use.
#[allow(dead_code)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove, no need to keep it around anymore


const readableStreamBody = new ReadableStream({
start: (controller) => {
streamChannel.onmessage = (res: Uint8Array) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be an ArrayBuffer iirc, and sometimes in the case where IPC fails to use the borwser fetch, it fallsback to Json serialization so this would be Array<number>

So we need to do some checks here, similar as what was done before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. I'll update that.

@adrieljss
Copy link
Contributor Author

Happy to say the stream that was implemented worked:
image

Something is wrong when i do content processing (like .json(), .text(), etc) so i'm trying to figure it out.

@adrieljss
Copy link
Contributor Author

adrieljss commented Mar 3, 2025

@amrbashir I added a new commit, can you check? I also fixed the content conversion bug said above, by converting the ArrayBuffer | number[] to Uint8Array (the content reader/consumer on Response doesn't like non-Uint8Arrays). So far it works fine when I tested it.

@adrieljss
Copy link
Contributor Author

I also removed ReqwestResponse completely acafdd8, it's never read on Rust and is not needed in guest-js.

@amrbashir
Copy link
Member

LGTM, now you just need to run cargo fmt and pnpm format, and a change file in .changes directory and we should be good to go.

@adrieljss
Copy link
Contributor Author

adrieljss commented Mar 3, 2025

Done!

just a heads up,
image
Channel might not be supported on the browser (http://localhost:xxxx), but when ran as an application it was fine.

Maybe should just fallback to fetch when in a browser setting?

@amrbashir
Copy link
Member

that's out of the plugin scope users will need to add check and fallback if they wish so.

@amrbashir
Copy link
Member

looks like you need a final pnpm build to build the JS API

@adrieljss adrieljss changed the title Add stream support HTTP add stream support Mar 3, 2025
@DresAaron
Copy link

Really appreciate the great work! Any plans to release this feature soon?

@letscagefdn
Copy link

good job guys, when do you think this will be released ?

Copy link
Member

@lucasfernog lucasfernog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amrbashir i'll approve and merge this one, it works well
i think any further change should come after a change in tauri core too right?

@lucasfernog lucasfernog merged commit cb38f54 into tauri-apps:v2 Mar 8, 2025
150 checks passed
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.

6 participants