Section 8/161 menit

8. Custom AsyncSequence

8. Custom AsyncSequence

Menulis AsyncSequence custom yang benar membutuhkan pemahaman tentang cooperative cancellation dan memory management.

Protocol Requirements

swift
// AsyncSequence membutuhkan: Element, makeAsyncIterator(), dan AsyncIterator
// AsyncIterator membutuhkan: next() async throws -> Element?

// Pattern paling umum: struct sequence + struct iterator
struct CountdownSequence: AsyncSequence {
    typealias Element = Int

    let from: Int
    let delaySeconds: Double

    func makeAsyncIterator() -> CountdownIterator {
        CountdownIterator(current: from, delay: delaySeconds)
    }
}

struct CountdownIterator: AsyncIteratorProtocol {
    var current: Int
    let delay: Double

    // next() dipanggil setiap `for await` iteration
    mutating func next() async throws -> Int? {
        guard current > 0 else { return nil }  // nil = sequence selesai

        // Check cancellation sebelum operasi async
        try Task.checkCancellation()

        // Operasi async
        try await Task.sleep(for: .seconds(delay))

        // Check cancellation lagi setelah sleep
        try Task.checkCancellation()

        defer { current -= 1 }
        return current
    }
}

// Penggunaan
for try await count in CountdownSequence(from: 5, delaySeconds: 1.0) {
    print(count)  // 5, 4, 3, 2, 1
}

AsyncSequence dengan State dan Cancellation

swift
// AsyncSequence untuk monitoring file changes
struct FileChangeSequence: AsyncSequence {
    typealias Element = FileChange

    let url: URL

    func makeAsyncIterator() -> FileChangeIterator {
        FileChangeIterator(url: url)
    }
}

struct FileChangeIterator: AsyncIteratorProtocol {
    let url: URL
    private var stream: AsyncStream<FileChange>.Iterator?

    init(url: URL) {
        self.url = url
        // Setup stream di init
        let (stream, continuation) = AsyncStream.makeStream(of: FileChange.self)
        self.stream = stream.makeAsyncIterator()

        // Start file monitoring — continuation.finish() saat deinit
        FileMonitor.start(url: url, continuation: continuation)
    }

    mutating func next() async -> FileChange? {
        // Delegate ke underlying AsyncStream
        await stream?.next()
    }
}

struct FileChange { let path: String; let type: ChangeType }
enum ChangeType { case created, modified, deleted }

class FileMonitor {
    static func start(url: URL, continuation: AsyncStream<FileChange>.Continuation) {
        // Platform-specific file monitoring
        // Di macOS: DispatchSource.makeFileSystemObjectSource
    }
}

AsyncSequence dengan Generics

swift
// Reusable paginated sequence
struct PaginatedSequence<Item: Decodable & Sendable>: AsyncSequence {
    typealias Element = Item

    let fetchPage: @Sendable (Int) async throws -> PageResult<Item>

    struct PageResult<T> {
        let items: [T]
        let hasMore: Bool
    }

    func makeAsyncIterator() -> PaginatedIterator {
        PaginatedIterator(fetchPage: fetchPage)
    }

    struct PaginatedIterator: AsyncIteratorProtocol {
        private var currentPage = 0
        private var buffer: [Item] = []
        private var hasMore = true
        private let fetchPage: @Sendable (Int) async throws -> PageResult<Item>

        init(fetchPage: @Sendable (Int) async throws -> PageResult<Item>) {
            self.fetchPage = fetchPage
        }

        mutating func next() async throws -> Item? {
            // Kembalikan dari buffer jika ada
            if !buffer.isEmpty {
                return buffer.removeFirst()
            }

            // Buffer kosong dan tidak ada halaman lagi
            guard hasMore else { return nil }

            try Task.checkCancellation()

            // Fetch halaman berikutnya
            let result = try await fetchPage(currentPage)
            currentPage += 1
            hasMore = result.hasMore
            buffer = Array(result.items.dropFirst())  // simpan sisa di buffer
            return result.items.first
        }
    }
}

// Penggunaan: iterasi semua user tanpa load semua ke memori
let allUsers = PaginatedSequence { page in
    let response = try await API.fetchUsers(page: page, pageSize: 50)
    return .init(items: response.users, hasMore: response.hasMore)
}

for try await user in allUsers {
    process(user)
}

struct APIUser: Decodable, Sendable { let id: String }
enum API { static func fetchUsers(page: Int, pageSize: Int) async throws -> (users: [APIUser], hasMore: Bool) { ([], false) } }
func process(_ user: APIUser) {}