Cookbook

File upload

The core reads the file through Fs, sends it in parts through Http and shows progress as a signal. The offline queue does the retrying, so no part of the recipe asks whether the network is up.

The recipe#

Show the Rust
core/src/upload.rs
/// Sends part `index` of `total`: `PUT /uploads/{upload}/parts/{index}`. Idempotent, so while the
/// device is offline it waits in the persisted queue and is replayed when the network returns.
#[undra::mutation(key = "upload:{upload}", idempotent)]
pub async fn put_part(
    ctx: &Ctx,
    upload: String,
    index: u32,
    data: Bytes,
) -> Result<u32, UploadError> {
    let url = net::url(ctx, &format!("/uploads/{upload}/parts/{index}"))?;
    let mut request = HttpRequest::new(HttpMethod::Put, url).with_body(data.0.clone());
    if let Some(key) = undra::query::idempotency_key() {
        request = request.with_header("Idempotency-Key", key.to_string());
    }
    net::ok(net::send(ctx, request).await?)?;
    Ok(u32::try_from(data.0.len()).unwrap_or(u32::MAX))
}
core/src/upload.rs
async fn send_parts(&self, ctx: &Ctx, id: u32, data: &[u8]) -> Result<(), UploadError> {
    let row = self.row(id).ok_or(UploadError::Unknown)?;
    let first = row.sent as usize / PART;
    for (index, part) in data.chunks(PART).enumerate().skip(first) {
        // Waits here while the device is offline, and carries on when the queue replays it.
        let sent = ctx
            .mutate::<PutPartMutation>((
                row.remote.clone(),
                u32::try_from(index).unwrap_or(u32::MAX),
                Bytes(part.to_vec()),
            ))
            .await?;
        // One row, one `Update` patch: this is the progress the screen draws.
        self.set_state(id, |row| row.sent = (index * PART) as u32 + sent);
    }
    let parts = u32::try_from(data.chunks(PART).count()).unwrap_or(u32::MAX);
    ctx.mutate::<CompleteUploadMutation>((row.remote, parts))
        .await?;
    Ok(())
}

On the platform#

A row per upload; progress and a retry button are plain reads and one call.

Show the Swift, Kotlin and TypeScript
swift
struct UploadsView: View {
    let uploads: Uploads

    var body: some View {
        ForEach(uploads.uploads, id: \.id) { row in
            ProgressView(value: Double(row.sent), total: Double(max(row.total, 1)))
            if case .failed(let reason) = row.state {
                Button("Retry (\(reason))") { Task { try? await uploads.retry(id: row.id) } }
            }
        }
    }
}
kotlin
class UploadsScreen(private val uploads: Uploads) {
    val rows: StateFlow<List<Upload>> = uploads.uploads

    fun progress(row: Upload): Float = row.sent.toFloat() / maxOf(row.total, 1u).toFloat()

    suspend fun retry(row: Upload) = uploads.retry(row.id)
}
tsx
export function UploadsView({ uploads }: { uploads: Uploads }) {
  const rows = useSignal(uploads.uploads);
  return rows.map((row) => (
    <progress key={row.id} value={row.sent} max={Math.max(row.total, 1)} />
  ));
}

What the tests prove#

Limits#

Source: examples/cookbook/core/src/upload.rs.

Edit this page on GitHub