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:
zarzet committed 2026-09-14 22:58:27 +07:00
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();
}
}