Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Distributed Tracing System

Skenario: Backend service yang butuh propagasi trace ID ke seluruh async call chain tanpa mengubah signature semua fungsi.

swift
// MARK: - Distributed Tracing dengan TaskLocal

import Foundation
import os

// Trace context yang di-propagate otomatis
enum TraceContext {
    @TaskLocal static var traceID: String = UUID().uuidString
    @TaskLocal static var spanID: String = UUID().uuidString
    @TaskLocal static var parentSpanID: String? = nil
    @TaskLocal static var serviceName: String = "unknown"
}

// Span: unit kerja yang bisa di-trace
@discardableResult
func withSpan<T>(
    _ name: String,
    attributes: [String: String] = [:],
    operation: () async throws -> T
) async rethrows -> T {
    let spanID = UUID().uuidString
    let startTime = ContinuousClock().now

    // Push span context ke TaskLocal untuk child tasks
    return try await TraceContext.$spanID.withValue(spanID) {
        try await TraceContext.$parentSpanID.withValue(TraceContext.spanID) {
            defer {
                let elapsed = ContinuousClock().now - startTime
                exportSpan(Span(
                    traceID: TraceContext.traceID,
                    spanID: spanID,
                    parentSpanID: TraceContext.parentSpanID,
                    name: name,
                    duration: elapsed,
                    attributes: attributes,
                    service: TraceContext.serviceName
                ))
            }
            return try await operation()
        }
    }
}

struct Span {
    let traceID: String
    let spanID: String
    let parentSpanID: String?
    let name: String
    let duration: ContinuousClock.Instant.Duration
    let attributes: [String: String]
    let service: String
}

func exportSpan(_ span: Span) {
    // Kirim ke Jaeger, Zipkin, atau OpenTelemetry collector
    os_log("Span: %{public}s [%{public}s] duration=%.2fms",
           span.name, span.traceID, span.duration.components.seconds * 1000)
}

// MARK: - Penggunaan: tracing tersebar tanpa ubah signature

actor OrderService {
    func processOrder(_ order: Order) async throws -> OrderResult {
        return try await withSpan("ProcessOrder", attributes: ["orderID": order.id]) {
            // traceID dan spanID otomatis tersedia di semua sub-operations
            let validated = try await withSpan("ValidateOrder") {
                try await validate(order)
            }

            let inventory = try await withSpan("CheckInventory") {
                try await checkInventory(for: validated)
            }

            let result = try await withSpan("ChargePayment") {
                try await charge(order: validated, inventory: inventory)
            }

            return result
        }
    }

    private func validate(_ order: Order) async throws -> Order {
        // TraceContext.traceID tersedia di sini tanpa parameter!
        let log = "Validating order \(order.id) trace=\(TraceContext.traceID)"
        _ = log
        return order
    }

    private func checkInventory(for order: Order) async throws -> InventorySnapshot {
        return InventorySnapshot()
    }

    private func charge(order: Order, inventory: InventorySnapshot) async throws -> OrderResult {
        return OrderResult()
    }
}

struct Order { let id: String }
struct InventorySnapshot {}
struct OrderResult {}

Use Case 2: Audio Engine dengan Custom Executor

Skenario: Digital Audio Workstation (DAW) app yang harus memproses audio di dedicated real-time thread dengan priority tertinggi, tanpa preemption.

swift
// MARK: - Real-time Audio Actor dengan Custom Executor

import AVFoundation

// Executor khusus untuk real-time audio processing
// Menggunakan dedicated thread dengan realtime priority
final class AudioRealTimeExecutor: SerialExecutor, @unchecked Sendable {
    private let thread: Thread
    private let runLoop: RunLoop
    private let semaphore = DispatchSemaphore(value: 0)
    private var pendingJobs: [(UnownedJob, UnownedSerialExecutor)] = []
    private let lock = NSLock()
    private var isRunning = true

    init() {
        var capturedRunLoop: RunLoop?
        let sem = DispatchSemaphore(value: 0)

        thread = Thread {
            capturedRunLoop = RunLoop.current
            sem.signal()
            // Keep thread alive
            while true {
                RunLoop.current.run(mode: .default, before: .distantFuture)
            }
        }

        // Set real-time priority (Audio workload)
        thread.threadPriority = 1.0
        thread.qualityOfService = .userInteractive
        thread.start()

        sem.wait()
        runLoop = capturedRunLoop!
    }

    func enqueue(_ job: consuming ExecutorJob) {
        let unownedJob = UnownedJob(job)
        let executor = asUnownedSerialExecutor()
        runLoop.perform { unownedJob.runSynchronously(on: executor) }
    }

    func asUnownedSerialExecutor() -> UnownedSerialExecutor {
        UnownedSerialExecutor(ordinary: self)
    }
}

// Actor audio yang berjalan di dedicated real-time thread
actor AudioEngine {
    private static let audioExecutor = AudioRealTimeExecutor()

    nonisolated var unownedExecutor: UnownedSerialExecutor {
        AudioEngine.audioExecutor.asUnownedSerialExecutor()
    }

    private var isRunning = false
    private var processingGraph: [AudioNode] = []
    private var sampleRate: Double = 44100
    private var bufferSize: Int = 512

    func start() {
        isRunning = true
        // Semua akses ke processing graph terjamin di audio thread
    }

    func stop() {
        isRunning = false
    }

    func addNode(_ node: AudioNode) {
        // Thread-safe karena actor isolation
        processingGraph.append(node)
    }

    // Dipanggil dari AVAudioEngine callback — harus sangat cepat
    func processBuffer(_ inputBuffer: AudioBuffer, outputBuffer: inout AudioBuffer) {
        guard isRunning else { return }

        // Process signal graph
        var signal = inputBuffer
        for node in processingGraph {
            signal = node.process(signal, sampleRate: sampleRate)
        }
        outputBuffer = signal
    }

    func updateParameter(_ param: AudioParameter, value: Float) {
        // Parameter update thread-safe via actor
        processingGraph
            .filter { $0.supportsParameter(param) }
            .forEach { $0.setParameter(param, value: value) }
    }
}

// Protocol untuk audio processing nodes
protocol AudioNode: AnyObject {
    func process(_ buffer: AudioBuffer, sampleRate: Double) -> AudioBuffer
    func supportsParameter(_ param: AudioParameter) -> Bool
    func setParameter(_ param: AudioParameter, value: Float)
}

struct AudioParameter: Hashable { let name: String }

// MARK: - SwiftUI Integration

@MainActor
@Observable
final class AudioEngineViewModel {
    private let engine = AudioEngine()
    var isPlaying = false
    var masterVolume: Float = 1.0 {
        didSet {
            Task {
                await engine.updateParameter(.init(name: "masterVolume"), value: masterVolume)
            }
        }
    }

    func startPlayback() async {
        await engine.start()
        isPlaying = true
    }

    func stopPlayback() async {
        await engine.stop()
        isPlaying = false
    }
}

Use Case 3: Typed Pagination dengan Custom AsyncSequence

Skenario: E-commerce app yang butuh scroll infinite dengan prefetch — data di-load secara streaming, tidak semua sekaligus.

swift
// MARK: - Typed Paginated AsyncSequence

struct PaginationConfig {
    let pageSize: Int
    let prefetchThreshold: Int  // mulai fetch sebelum habis
}

struct ProductPage {
    let products: [Product]
    let totalCount: Int
    let nextCursor: String?
}

struct Product: Identifiable, Sendable {
    let id: String
    let name: String
    let price: Double
}

// AsyncSequence yang handle pagination + prefetching
struct ProductFeed: AsyncSequence {
    typealias Element = Product

    let config: PaginationConfig
    let fetchPage: @Sendable (String?) async throws -> ProductPage

    func makeAsyncIterator() -> ProductFeedIterator {
        ProductFeedIterator(config: config, fetchPage: fetchPage)
    }
}

struct ProductFeedIterator: AsyncIteratorProtocol {
    private var buffer: [Product] = []
    private var nextCursor: String? = nil
    private var hasMore = true
    private var isFetching = false
    private var prefetchTask: Task<ProductPage, Error>?
    private let config: PaginationConfig
    private let fetchPage: @Sendable (String?) async throws -> ProductPage
    private var isFirstFetch = true

    init(config: PaginationConfig, fetchPage: @Sendable (String?) async throws -> ProductPage) {
        self.config = config
        self.fetchPage = fetchPage
    }

    mutating func next() async throws -> Product? {
        // Kembalikan dari buffer jika ada
        if !buffer.isEmpty {
            let product = buffer.removeFirst()

            // Trigger prefetch ketika buffer hampir habis
            if buffer.count <= config.prefetchThreshold && hasMore && prefetchTask == nil {
                let cursor = nextCursor
                let fetch = fetchPage
                prefetchTask = Task { try await fetch(cursor) }
            }

            return product
        }

        // Buffer kosong
        guard hasMore else { return nil }

        try Task.checkCancellation()

        // Tunggu prefetch task jika ada, atau fetch baru
        let page: ProductPage
        if let prefetch = prefetchTask {
            page = try await prefetch.value
            prefetchTask = nil
        } else {
            page = try await fetchPage(nextCursor)
        }

        nextCursor = page.nextCursor
        hasMore = page.nextCursor != nil
        buffer = Array(page.products.dropFirst())

        return page.products.first
    }
}

// MARK: - SwiftUI Usage

@MainActor
@Observable
final class ProductListViewModel {
    var products: [Product] = []
    var isLoading = false
    var error: String?

    private var feedIterator: ProductFeedIterator?
    private var loadMoreTask: Task<Void, Never>?

    func startLoading() {
        let feed = ProductFeed(
            config: PaginationConfig(pageSize: 20, prefetchThreshold: 5)
        ) { cursor in
            try await ProductAPI.fetchProducts(cursor: cursor, pageSize: 20)
        }
        feedIterator = feed.makeAsyncIterator()
        loadMore()
    }

    func loadMore() {
        guard !isLoading else { return }
        isLoading = true

        loadMoreTask = Task { [weak self] in
            guard var iterator = self?.feedIterator else { return }
            var newProducts: [Product] = []

            // Load satu halaman worth of products
            do {
                for _ in 0..<20 {
                    guard let product = try await iterator.next() else { break }
                    newProducts.append(product)
                }
                self?.feedIterator = iterator
                self?.products.append(contentsOf: newProducts)
            } catch {
                self?.error = error.localizedDescription
            }

            self?.isLoading = false
        }
    }
}

enum ProductAPI {
    static func fetchProducts(cursor: String?, pageSize: Int) async throws -> ProductPage {
        // Simulate API call
        try await Task.sleep(for: .milliseconds(500))
        let products = (0..<pageSize).map { i in
            Product(id: UUID().uuidString, name: "Product \(i)", price: Double(i) * 9.99)
        }
        return ProductPage(
            products: products,
            totalCount: 1000,
            nextCursor: products.count == pageSize ? UUID().uuidString : nil
        )
    }
}