mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-09 09:31:10 +02:00
feat(backend): migrate production backend to Rust
Replace the Go backend with the Rust workspace and route Android/iOS through UniFFI bindings. Include the migrated extension runtime, network, providers, media metadata, downloads and library operations, with version 5.0.0+147. Remove Go sources, adapters, native build selection and CI dependencies. Build Rust unconditionally and lock the iOS Rust pod using a relative path. Retain legacy source and migration evidence in a local ignored archive. Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI build checkpoint; no new APK or iOS application build for this cleanup. Known follow-up: URL/URLSearchParams globals are missing from the Rust JS runtime. A controlled extension replay confirms a URL-resolution regression; this commit does not fix that runtime gap.
This commit is contained in:
1 parent
1da0da18a6
commit
aa99726439
452 files changed
+64417
-65088
No files matched your search
@@ -0,0 +1,429 @@
|
||||
//! Go-compatible sparse download progress, shared by every native consumer.
|
||||
|
||||
mod reporter;
|
||||
mod subscription;
|
||||
|
||||
pub use reporter::{ItemProgressWriter, ItemTransferProgressReporter};
|
||||
pub use subscription::ProgressSubscription;
|
||||
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
pub const UPDATE_BYTES: i64 = 128 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct ItemProgress {
|
||||
pub item_id: String,
|
||||
pub bytes_total: i64,
|
||||
pub bytes_received: i64,
|
||||
pub progress: f64,
|
||||
pub speed_mbps: f64,
|
||||
pub is_downloading: bool,
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub stage: String,
|
||||
#[serde(skip)]
|
||||
revision: i64,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct BridgeState {
|
||||
bytes_bucket: i64,
|
||||
total: i64,
|
||||
percent: i64,
|
||||
speed_tenths: i64,
|
||||
downloading: bool,
|
||||
status: String,
|
||||
stage: String,
|
||||
}
|
||||
|
||||
impl ItemProgress {
|
||||
fn bridge(&self) -> BridgeState {
|
||||
let progress = if self.progress.is_nan() || self.progress <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
self.progress.min(1.0)
|
||||
};
|
||||
let speed = if self.speed_mbps.is_nan() || self.speed_mbps <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
self.speed_mbps
|
||||
};
|
||||
BridgeState {
|
||||
bytes_bucket: self.bytes_received / UPDATE_BYTES,
|
||||
total: self.bytes_total,
|
||||
percent: (progress * 100.0).round() as i64,
|
||||
speed_tenths: rounded_speed(speed * 10.0),
|
||||
downloading: self.is_downloading,
|
||||
status: self.status.clone(),
|
||||
stage: self.stage.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finite(&self) -> bool {
|
||||
self.progress.is_finite() && self.speed_mbps.is_finite()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProgressError {
|
||||
Closed,
|
||||
SubscriptionClosed,
|
||||
}
|
||||
|
||||
impl fmt::Display for ProgressError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
Self::Closed => "download progress registry closed",
|
||||
Self::SubscriptionClosed => "download progress subscription closed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProgressError {}
|
||||
|
||||
struct State {
|
||||
items: BTreeMap<String, ItemProgress>,
|
||||
removed: BTreeMap<String, i64>,
|
||||
seq: i64,
|
||||
reset: i64,
|
||||
dirty: bool,
|
||||
cached: String,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
items: BTreeMap::new(),
|
||||
removed: BTreeMap::new(),
|
||||
seq: 0,
|
||||
reset: 0,
|
||||
dirty: true,
|
||||
cached: "{\"items\":{}}".into(),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn check(&self) -> Result<(), ProgressError> {
|
||||
if self.closed {
|
||||
Err(ProgressError::Closed)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self, changed: &Condvar) -> i64 {
|
||||
self.seq = self.seq.wrapping_add(1);
|
||||
self.dirty = true;
|
||||
changed.notify_all();
|
||||
self.seq
|
||||
}
|
||||
|
||||
fn delta(&self, since: i64) -> String {
|
||||
if since >= self.seq {
|
||||
return String::new();
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Delta<'a> {
|
||||
seq: i64,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
reset: bool,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
items: BTreeMap<&'a String, &'a ItemProgress>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
removed: Vec<&'a String>,
|
||||
}
|
||||
let reset = since <= 0 || since < self.reset;
|
||||
let items: BTreeMap<_, _> = self
|
||||
.items
|
||||
.iter()
|
||||
.filter(|(_, item)| reset || item.revision > since)
|
||||
.collect();
|
||||
// serde_json represents non-finite floats as null; encoding/json fails
|
||||
// the whole document instead. Preserve Go's empty/fallback responses.
|
||||
if items.values().any(|item| !item.finite()) {
|
||||
return String::new();
|
||||
}
|
||||
let removed = if reset {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.removed
|
||||
.iter()
|
||||
.filter(|(_, revision)| **revision > since)
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
};
|
||||
serde_json::to_string(&Delta {
|
||||
seq: self.seq,
|
||||
reset,
|
||||
items,
|
||||
removed,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Shared {
|
||||
state: Mutex<State>,
|
||||
changed: Condvar,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProgressRegistry {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl ProgressRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn start(&self, id: &str) -> Result<(), ProgressError> {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
let revision = state.next(&self.shared.changed);
|
||||
state.items.insert(
|
||||
id.to_owned(),
|
||||
ItemProgress {
|
||||
item_id: id.to_owned(),
|
||||
bytes_total: 0,
|
||||
bytes_received: 0,
|
||||
progress: 0.0,
|
||||
speed_mbps: 0.0,
|
||||
is_downloading: false,
|
||||
status: "preparing".into(),
|
||||
stage: String::new(),
|
||||
revision,
|
||||
},
|
||||
);
|
||||
state.removed.remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
id: &str,
|
||||
update: impl FnOnce(&mut ItemProgress),
|
||||
) -> Result<(), ProgressError> {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
if let Some(item) = state.items.get_mut(id) {
|
||||
let before = item.bridge();
|
||||
update(item);
|
||||
if item.bridge() != before {
|
||||
let revision = state.next(&self.shared.changed);
|
||||
state.items.get_mut(id).expect("existing progress").revision = revision;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn preparing(&self, id: &str, stage: &str) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| {
|
||||
item.progress = 0.0;
|
||||
item.bytes_received = 0;
|
||||
item.bytes_total = 0;
|
||||
item.speed_mbps = 0.0;
|
||||
item.is_downloading = true;
|
||||
item.status = "preparing".into();
|
||||
item.stage = stage.to_owned();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn downloading(&self, id: &str) -> Result<(), ProgressError> {
|
||||
self.update(id, mark_downloading)
|
||||
}
|
||||
|
||||
pub fn set_total(&self, id: &str, total: i64) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| item.bytes_total = total)
|
||||
}
|
||||
|
||||
pub fn set_received(&self, id: &str, received: i64) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| set_received(item, received))
|
||||
}
|
||||
|
||||
pub fn set_received_with_speed(
|
||||
&self,
|
||||
id: &str,
|
||||
received: i64,
|
||||
speed: f64,
|
||||
) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| {
|
||||
item.speed_mbps = speed;
|
||||
set_received(item, received);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_progress(
|
||||
&self,
|
||||
id: &str,
|
||||
progress: f64,
|
||||
received: i64,
|
||||
total: i64,
|
||||
) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| {
|
||||
let has_bytes = received > 0 || total > 0;
|
||||
item.progress = if item.status != "preparing" || has_bytes || progress >= 1.0 {
|
||||
progress
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if received > 0 {
|
||||
item.bytes_received = received;
|
||||
}
|
||||
if total > 0 {
|
||||
item.bytes_total = total;
|
||||
}
|
||||
if has_bytes || progress >= 1.0 || item.status == "downloading" {
|
||||
mark_downloading(item);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finalizing(&self, id: &str) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| {
|
||||
item.progress = 1.0;
|
||||
item.status = "finalizing".into();
|
||||
item.stage.clear();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn complete(&self, id: &str) -> Result<(), ProgressError> {
|
||||
self.update(id, |item| {
|
||||
item.progress = 1.0;
|
||||
item.is_downloading = false;
|
||||
item.status = "completed".into();
|
||||
item.stage.clear();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: &str) -> Result<(), ProgressError> {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
if state.items.remove(id).is_some() {
|
||||
let seq = state.next(&self.shared.changed);
|
||||
state.removed.insert(id.to_owned(), seq);
|
||||
if state.removed.len() > 512 {
|
||||
let mut revisions: Vec<_> = state.removed.values().copied().collect();
|
||||
revisions.sort_unstable();
|
||||
let cutoff = revisions[revisions.len() / 2];
|
||||
state.removed.retain(|_, revision| *revision > cutoff);
|
||||
state.reset = state.reset.max(cutoff);
|
||||
}
|
||||
}
|
||||
// Even removing a missing ID invalidates the snapshot cache in Go.
|
||||
state.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), ProgressError> {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
state.items.clear();
|
||||
state.removed.clear();
|
||||
state.reset = state.next(&self.shared.changed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn item(&self, id: &str) -> Result<String, ProgressError> {
|
||||
let state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
Ok(match state.items.get(id) {
|
||||
Some(item) if item.finite() => serde_json::to_string(item).unwrap_or_default(),
|
||||
Some(_) => String::new(),
|
||||
None => "{}".into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Result<String, ProgressError> {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
if state.dirty {
|
||||
if state.items.values().any(|item| !item.finite()) {
|
||||
return Ok("{\"items\":{}}".into());
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Snapshot<'a> {
|
||||
items: &'a BTreeMap<String, ItemProgress>,
|
||||
}
|
||||
state.cached = serde_json::to_string(&Snapshot {
|
||||
items: &state.items,
|
||||
})
|
||||
.expect("finite progress JSON");
|
||||
state.dirty = false;
|
||||
}
|
||||
Ok(state.cached.clone())
|
||||
}
|
||||
|
||||
pub fn delta(&self, since: i64) -> Result<String, ProgressError> {
|
||||
let state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
Ok(state.delta(since))
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> Result<ProgressSubscription, ProgressError> {
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.expect("progress state lock")
|
||||
.check()?;
|
||||
Ok(ProgressSubscription::new(Arc::clone(&self.shared)))
|
||||
}
|
||||
|
||||
pub fn wait_delta(&self, since: i64, timeout_ms: i64) -> Result<String, ProgressError> {
|
||||
self.subscribe()?.wait_delta(since, timeout_ms)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
state.closed = true;
|
||||
state.items.clear();
|
||||
state.removed.clear();
|
||||
state.cached.clear();
|
||||
self.shared.changed.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgressRegistry {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_downloading(item: &mut ItemProgress) {
|
||||
item.is_downloading = true;
|
||||
item.status = "downloading".into();
|
||||
item.stage.clear();
|
||||
}
|
||||
|
||||
fn set_received(item: &mut ItemProgress, received: i64) {
|
||||
item.bytes_received = received;
|
||||
if item.bytes_total > 0 {
|
||||
item.progress = received as f64 / item.bytes_total as f64;
|
||||
}
|
||||
if received > 0 {
|
||||
mark_downloading(item);
|
||||
}
|
||||
}
|
||||
|
||||
fn rounded_speed(value: f64) -> i64 {
|
||||
let value = value.round();
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
if value >= 9_223_372_036_854_775_808.0 {
|
||||
return i64::MIN;
|
||||
}
|
||||
#[cfg(target_arch = "arm")]
|
||||
if value >= 18_446_744_073_709_551_616.0 || !value.is_finite() {
|
||||
// Go ARM32's int64 conversion delegates out-of-range values to _d2v.
|
||||
return (u64::from(value as u32) << 32) as i64;
|
||||
} else if value >= 9_223_372_036_854_775_808.0 {
|
||||
return value as u64 as i64;
|
||||
}
|
||||
value as i64
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use super::{ProgressRegistry, UPDATE_BYTES};
|
||||
use crate::cancellation::CancellationRegistry;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct ItemTransferProgressReporter {
|
||||
progress: Arc<ProgressRegistry>,
|
||||
id: String,
|
||||
state: Mutex<ReportState>,
|
||||
}
|
||||
|
||||
struct ReportState {
|
||||
received: i64,
|
||||
total: i64,
|
||||
at: Instant,
|
||||
}
|
||||
|
||||
impl ItemTransferProgressReporter {
|
||||
pub fn new(progress: Arc<ProgressRegistry>, id: &str, received: i64, total: i64) -> Self {
|
||||
Self {
|
||||
progress,
|
||||
id: id.to_owned(),
|
||||
state: Mutex::new(ReportState {
|
||||
received,
|
||||
total,
|
||||
at: Instant::now(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report(&self, received: i64, total: i64) {
|
||||
self.report_at(received, total, Instant::now());
|
||||
}
|
||||
|
||||
fn report_at(&self, received: i64, total: i64, now: Instant) {
|
||||
if self.id.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.lock().expect("progress reporter lock");
|
||||
let delta = received.wrapping_sub(state.received);
|
||||
if (0..UPDATE_BYTES).contains(&delta)
|
||||
&& total == state.total
|
||||
&& now.saturating_duration_since(state.at) < Duration::from_millis(250)
|
||||
{
|
||||
return;
|
||||
}
|
||||
*state = ReportState {
|
||||
received,
|
||||
total,
|
||||
at: now,
|
||||
};
|
||||
if total > 0 {
|
||||
let _ = self.progress.set_progress(
|
||||
&self.id,
|
||||
received as f64 / total as f64,
|
||||
received,
|
||||
total,
|
||||
);
|
||||
} else {
|
||||
let _ = self.progress.set_received(&self.id, received);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter for media writers. Network transfer reporters deliberately do not
|
||||
/// synthesize speed: the existing Go writer owns that separate behavior.
|
||||
pub struct ItemProgressWriter<W> {
|
||||
writer: W,
|
||||
id: String,
|
||||
progress: Arc<ProgressRegistry>,
|
||||
cancellation: Arc<CancellationRegistry>,
|
||||
current: i64,
|
||||
last_reported: i64,
|
||||
last_time: Instant,
|
||||
last_bytes: i64,
|
||||
}
|
||||
|
||||
impl<W> ItemProgressWriter<W> {
|
||||
pub fn new(
|
||||
writer: W,
|
||||
id: &str,
|
||||
progress: Arc<ProgressRegistry>,
|
||||
cancellation: Arc<CancellationRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
id: id.to_owned(),
|
||||
progress,
|
||||
cancellation,
|
||||
current: 0,
|
||||
last_reported: 0,
|
||||
last_time: Instant::now(),
|
||||
last_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> W {
|
||||
self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ItemProgressWriter<W> {
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||
if !self.id.is_empty() {
|
||||
match self.cancellation.is_cancelled(&self.id) {
|
||||
Ok(true) => return Err(io::Error::other("download cancelled")),
|
||||
Err(error) => return Err(io::Error::other(error)),
|
||||
Ok(false) => {}
|
||||
}
|
||||
}
|
||||
let count = self.writer.write(bytes)?;
|
||||
self.current = self.current.wrapping_add(count as i64);
|
||||
if self.last_reported == 0 || self.current.wrapping_sub(self.last_reported) >= UPDATE_BYTES
|
||||
{
|
||||
let now = Instant::now();
|
||||
let elapsed = now.saturating_duration_since(self.last_time).as_secs_f64();
|
||||
let speed = if elapsed > 0.0 {
|
||||
self.current.wrapping_sub(self.last_bytes) as f64 / (1024.0 * 1024.0) / elapsed
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let _ = self
|
||||
.progress
|
||||
.set_received_with_speed(&self.id, self.current, speed);
|
||||
self.last_reported = self.current;
|
||||
self.last_bytes = self.current;
|
||||
self.last_time = now;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn coalescing_flushes_threshold_interval_total_changes_and_rollbacks() {
|
||||
let progress = Arc::new(ProgressRegistry::new());
|
||||
progress.start("example").unwrap();
|
||||
let reporter =
|
||||
ItemTransferProgressReporter::new(Arc::clone(&progress), "example", 0, 1 << 20);
|
||||
let now = reporter.state.lock().unwrap().at;
|
||||
let received = || {
|
||||
serde_json::from_str::<serde_json::Value>(&progress.item("example").unwrap()).unwrap()["bytes_received"].as_i64().unwrap()
|
||||
};
|
||||
reporter.report_at(65536, 1 << 20, now);
|
||||
assert_eq!(received(), 0);
|
||||
reporter.report_at(UPDATE_BYTES, 1 << 20, now);
|
||||
assert_eq!(received(), UPDATE_BYTES);
|
||||
reporter.report_at(UPDATE_BYTES + 1, 1 << 20, now + Duration::from_millis(250));
|
||||
assert_eq!(received(), UPDATE_BYTES + 1);
|
||||
reporter.report_at(UPDATE_BYTES + 2, 2 << 20, now + Duration::from_millis(250));
|
||||
assert_eq!(received(), UPDATE_BYTES + 2);
|
||||
reporter.report_at(1, 2 << 20, now + Duration::from_millis(250));
|
||||
assert_eq!(received(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use super::{ProgressError, Shared};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct ProgressSubscription {
|
||||
shared: Arc<Shared>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl ProgressSubscription {
|
||||
pub(super) fn new(shared: Arc<Shared>) -> Self {
|
||||
Self {
|
||||
shared,
|
||||
closed: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// No event is consumed: independent UI/background listeners use their own
|
||||
/// sequence, and every waiter sees the same revision. Close wakes only this
|
||||
/// subscription semantically; other waiters continue against their deadline.
|
||||
pub fn wait_delta(&self, since: i64, timeout_ms: i64) -> Result<String, ProgressError> {
|
||||
let timeout = Duration::from_millis(if timeout_ms <= 0 {
|
||||
15_000
|
||||
} else {
|
||||
timeout_ms.min(60_000)
|
||||
} as u64);
|
||||
let started = Instant::now();
|
||||
let mut state = self.shared.state.lock().expect("progress state lock");
|
||||
loop {
|
||||
state.check()?;
|
||||
if self.closed.load(Ordering::Acquire) {
|
||||
return Err(ProgressError::SubscriptionClosed);
|
||||
}
|
||||
if since < state.seq {
|
||||
return Ok(state.delta(since));
|
||||
}
|
||||
let remaining = timeout.saturating_sub(started.elapsed());
|
||||
if remaining.is_zero() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
(state, _) = self
|
||||
.shared
|
||||
.changed
|
||||
.wait_timeout(state, remaining)
|
||||
.expect("progress wait lock");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let _state = self.shared.state.lock().expect("progress state lock");
|
||||
self.closed.store(true, Ordering::Release);
|
||||
self.shared.changed.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgressSubscription {
|
||||
fn drop(&mut self) {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user