Section 4/211 menit

4. Worker — actor

4. Worker — actor

swift
import Foundation

// Worker bertanggung jawab atas satu hal saja: ambil data dari sumber eksternal.
// Dijadikan actor untuk memastikan tidak ada concurrent access ke state internalnya
// (misalnya URLSession, cache, atau connection pool).
actor UserWorker {

    // URLSession sudah thread-safe (Sendable), tapi kita tetap simpan di dalam
    // actor agar konfigurasi session tidak bisa diubah dari luar secara bersamaan.
    private let session: URLSession
    private let baseURL: URL

    init(
        session: URLSession = .shared,
        baseURL: URL = URL(string: "https://jsonplaceholder.typicode.com")!
    ) {
        self.session = session
        self.baseURL = baseURL
    }

    // Fungsi ini berjalan di dalam isolation domain actor ini (background thread).
    // Return type [User] adalah Sendable sehingga aman dikirim ke caller (Interactor).
    func fetchUsers() async throws -> [User] {
        let url = baseURL.appendingPathComponent("/users")
        let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)

        // await di sini melepas thread actor sementara — actor bisa melayani
        // permintaan lain selagi menunggu network response.
        let (data, response) = try await session.data(for: request)

        // Validasi HTTP response — tidak butuh await, ini synchronous check.
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw WorkerError.invalidResponse
        }

        // JSONDecoder.decode adalah operasi CPU-bound.
        // Karena kita berada di actor (bukan main thread), ini aman dan tidak
        // memblokir UI.
        let dtos = try JSONDecoder().decode([UserDTO].self, from: data)

        // Transformasi dari DTO (raw API model) ke Domain Model.
        // Transformasi ini juga terjadi di background — efisien.
        return dtos.map { dto in
            User(
                id: String(dto.id),
                name: dto.name,
                email: dto.email,
                avatarURL: URL(string: "https://i.pravatar.cc/150?u=\(dto.id)")
            )
        }
    }

    enum WorkerError: Error, LocalizedError {
        case invalidResponse

        var errorDescription: String? {
            switch self {
            case .invalidResponse: return "Server mengembalikan respons tidak valid."
            }
        }
    }
}

// MARK: - DTO (Data Transfer Object)

// DTO hanya digunakan di Worker — tidak perlu Sendable karena
// tidak pernah keluar dari isolation domain Worker.
private struct UserDTO: Decodable {
    let id: Int
    let name: String
    let email: String
}

Penjelasan poin penting:

  • actor UserWorker menjamin bahwa session dan baseURL tidak diakses secara concurrent dari beberapa thread. Meskipun URLSession sudah Sendable, membungkusnya dalam actor adalah praktik yang baik untuk skalabilitas (misalnya saat nanti menambah retry logic atau request queuing).
  • UserDTO adalah private — detail implementasi network tidak bocor ke layer lain. Hanya User (domain model) yang keluar dari Worker.
  • await session.data(for:) melepas isolation domain Actor sementara — inilah kekuatan Actor: bisa melayani permintaan lain selagi menunggu I/O.