{"id":27902828,"url":"https://github.com/hecrj/sipper","last_synced_at":"2025-05-05T21:39:06.258Z","repository":{"id":276205105,"uuid":"928562781","full_name":"hecrj/sipper","owner":"hecrj","description":"A type-safe future that can stream progress for Rust","archived":false,"fork":false,"pushed_at":"2025-03-13T23:19:37.000Z","size":76,"stargazers_count":33,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-30T18:52:03.572Z","etag":null,"topics":["async","futures","notify","progress","stream"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hecrj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"hecrj"}},"created_at":"2025-02-06T20:49:20.000Z","updated_at":"2025-04-22T03:09:44.000Z","dependencies_parsed_at":"2025-02-06T22:19:09.774Z","dependency_job_id":"910fd7a3-fc17-4a3e-b085-a0437567105e","html_url":"https://github.com/hecrj/sipper","commit_stats":null,"previous_names":["hecrj/sipper"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hecrj%2Fsipper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hecrj%2Fsipper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hecrj%2Fsipper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hecrj%2Fsipper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hecrj","download_url":"https://codeload.github.com/hecrj/sipper/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252581189,"owners_count":21771477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","futures","notify","progress","stream"],"created_at":"2025-05-05T21:39:02.367Z","updated_at":"2025-05-05T21:39:06.252Z","avatar_url":"https://github.com/hecrj.png","language":"Rust","funding_links":["https://github.com/sponsors/hecrj"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Sipper\n\n[![Documentation](https://docs.rs/sipper/badge.svg)](https://docs.rs/sipper)\n[![Crates.io](https://img.shields.io/crates/v/sipper.svg)](https://crates.io/crates/sipper)\n[![License](https://img.shields.io/crates/l/sipper.svg)](https://github.com/hecrj/sipper/blob/master/LICENSE)\n[![Downloads](https://img.shields.io/crates/d/sipper.svg)](https://crates.io/crates/sipper)\n[![Test Status](https://img.shields.io/github/actions/workflow/status/hecrj/sipper/test.yml?branch=master\u0026event=push\u0026label=test)](https://github.com/hecrj/sipper/actions)\n\nA sipper is a type-safe [`Future`] that can [`Stream`] progress.\n\u003c/div\u003e\n\nEffectively, a [`Sipper`] combines a [`Future`] and a [`Stream`]\ntogether to represent an asynchronous task that produces some `Output`\nand notifies of some `Progress`, without both types being necessarily the\nsame.\n\nIn fact, a [`Sipper`] implements both the [`Future`] and the [`Stream`] traits—which\ngives you all the great combinators from [`FutureExt`] and [`StreamExt`] for free.\n\nGenerally, [`Sipper`] should be chosen over [`Stream`] when the final value produced—the\nend of the task—is important and inherently different from the other values.\n\n# An Example\nAn example of this could be a file download. When downloading a file, the progress\nthat must be notified is normally a bunch of statistics related to the download; but\nwhen the download finishes, the contents of the file need to also be provided.\n\n## The Uncomfy Stream\nWith a [`Stream`], you must create some kind of type that unifies both states of the\ndownload:\n\n```rust\nuse futures::Stream;\n\nstruct File(Vec\u003cu8\u003e);\n\ntype Progress = u32;\n\nenum Download {\n    Running(Progress),\n    Done(File)\n}\n\nfn download(url: \u0026str) -\u003e impl Stream\u003cItem = Download\u003e {\n    // ...\n}\n```\n\nIf we now wanted to notify progress and—at the same time—do something with\nthe final `File`, we'd need to juggle with the [`Stream`]:\n\n```rust\nuse futures::StreamExt;\n\nasync fn example() {\n    let mut file_download = download(\"https://iced.rs/logo.svg\").boxed();\n\n    while let Some(download) = file_download.next().await {\n        match download {\n            Download::Running(progress) =\u003e {\n                println!(\"{progress}%\");\n            }\n            Download::Done(file) =\u003e {\n                // Do something with file...\n                // We are nested, and there are no compiler guarantees\n                // this will ever be reached. And how many times?\n            }\n        }\n    }\n}\n```\n\nWhile we could rewrite the previous snippet using `loop`, `expect`, and `break` to get the\nfinal file out of the [`Stream`], we would still be introducing runtime errors and, simply put,\nworking around the fact that a [`Stream`] does not encode the idea of a final value.\n\n## The Chad Sipper\nA [`Sipper`] can precisely describe this dichotomy in a type-safe way:\n\n```rust\nuse sipper::Sipper;\n\nstruct File(Vec\u003cu8\u003e);\n\ntype Progress = u32;\n\nfn download(url: \u0026str) -\u003e impl Sipper\u003cFile, Progress\u003e {\n    // ...\n}\n```\n\nWhich can then be easily ~~used~~ sipped:\n\n```rust\nasync fn example() -\u003e File {\n    let mut download = download(\"https://iced.rs/logo.svg\").pin();\n\n    // A sipper is a stream!\n    // `Sipper::sip` is actually just an alias of `Stream::next`\n    while let Some(progress) = download.sip().await {\n        println!(\"{progress}%\");\n    }\n\n    // A sipper is also a future!\n    let logo = download.await;\n\n    // We are guaranteed to have a File here!\n    logo\n}\n```\n\n## The Delicate Straw\nHow about error handling? Fear not! A [`Straw`] is a [`Sipper`] that can fail. What would\nour download example look like with an error sprinkled in?\n\n```rust\nenum Error {\n    Failed,\n}\n\nfn try_download(url: \u0026str) -\u003e impl Straw\u003cFile, Progress, Error\u003e {\n    // ...\n}\n\nasync fn example() -\u003e Result\u003cFile, Error\u003e {\n    let mut download = try_download(\"https://iced.rs/logo.svg\").pin();\n \n    while let Some(progress) = download.sip().await {\n        println!(\"{progress}%\");\n    }\n \n    let logo = download.await?;\n \n    // We are guaranteed to have a File here!\n    Ok(logo)\n}\n```\n\nPretty much the same! It's quite easy to add error handling to an existing [`Sipper`].\nIn fact, [`Straw`] is actually just an extension trait of a [`Sipper`] with a `Result` as output.\nTherefore, all the [`Sipper`] methods are available for [`Straw`] as well. It's just nicer to write!\n\n## The Great Builder\nYou can build a [`Sipper`] with the [`sipper`] function. It takes a closure that receives\na [`Sender`]—for sending progress updates—and must return a [`Future`] producing the output.\n\n```rust\nfn download(url: \u0026str) -\u003e impl Sipper\u003cFile, Progress\u003e {\n    sipper(async move |mut sender| {\n        // Perform async request here...\n        let download = /* ... */;\n\n        while let Some(chunk) = download.chunk().await {\n            // ...\n            // Send updates when needed\n            sender.send(/* ... */).await;\n\n        }\n\n        File(/* ... */)\n    })\n}\n```\n\nFurthermore, [`Sipper`] has no required methods and is just an extension trait of a\n[`Future`] and [`Stream`] combo. This means you can come up with new ways to build a\n[`Sipper`] by implementing the async traits on any of your types. Additionally,\nany foreign type that implements both is already one.\n\n## The Fancy Composition\nA [`Sipper`] supports a bunch of methods for easy composition; like [`with`], [`filter_with`],\nand [`run`].\n\nFor instance, let's say we wanted to build a new function that downloads a bunch of files\ninstead of just one:\n\n```rust\nfn download_all(urls: \u0026[\u0026str]) -\u003e impl Sipper\u003cVec\u003cFile\u003e, (usize, Progress)\u003e {\n    sipper(async move |sender| {\n        let mut files = Vec::new();\n\n        for (id, url) in urls.iter().enumerate() {\n            let file = download(url)\n                .with(move |progress| (id, progress))\n                .run(\u0026sender)\n                .await;\n\n            files.push(file);\n        }\n\n        files\n    })\n}\n```\n\nAs you can see, we just leverage [`with`] to combine the download index with the progress\nand call [`run`] to drive the [`Sipper`] to completion—notifying properly through the [`Sender`].\n\nOf course, this example will download files sequentially; but, since [`run`] returns a simple\n[`Future`], a proper collection like [`FuturesOrdered`] could be used just as easily—if not\nmore! Take a look:\n\n```rust\nuse futures::stream::{FuturesOrdered, StreamExt};\n\nfn download_all(urls: \u0026[\u0026str]) -\u003e impl Sipper\u003cVec\u003cFile\u003e, (usize, Progress)\u003e {\n    sipper(|sender| {\n        urls.iter()\n            .enumerate()\n            .map(|(id, url)| {\n                download(url)\n                    .with(move |progress| (id, progress))\n                    .run(\u0026sender)\n            })\n            .collect::\u003cFuturesOrdered\u003c_\u003e\u003e()\n            .collect()\n    })\n}\n```\n\n[`Sipper`]: https://docs.rs/sipper/latest/sipper/trait.Sipper.html\n[`Straw`]: https://docs.rs/sipper/latest/sipper/trait.Straw.html\n[`Sender`]: https://docs.rs/sipper/latest/sipper/struct.Sender.html\n[`Future`]: https://docs.rs/futures/0.3.31/futures/future/trait.Future.html\n[`Stream`]: https://docs.rs/futures/0.3.31/futures/stream/trait.Stream.html\n[`FutureExt`]: https://docs.rs/futures/0.3.31/futures/future/trait.FutureExt.html\n[`StreamExt`]: https://docs.rs/futures/0.3.31/futures/stream/trait.StreamExt.html\n[`Sink`]: https://docs.rs/futures/0.3.31/futures/sink/trait.Sink.html\n[`FuturesOrdered`]: https://docs.rs/futures/0.3.31/futures/stream/struct.FuturesOrdered.html\n[`sipper`]: https://docs.rs/sipper/latest/sipper/fn.sipper.html\n[`with`]: https://docs.rs/sipper/latest/sipper/trait.Sipper.html#method.with\n[`filter_with`]: https://docs.rs/sipper/latest/sipper/trait.Sipper.html#method.filter_with\n[`run`]: https://docs.rs/sipper/latest/sipper/trait.Sipper.html#method.run\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhecrj%2Fsipper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhecrj%2Fsipper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhecrj%2Fsipper/lists"}