Section 4/161 menit

4. TaskLocal Values

4. TaskLocal Values

@TaskLocal adalah storage yang otomatis di-propagate ke semua child tasks dan melalui await boundaries — mirip thread-local storage tapi untuk task tree.

Deklarasi dan Penggunaan

swift
// MARK: - TaskLocal Declarations

enum Tracing {
    @TaskLocal static var traceID: String = ""
    @TaskLocal static var spanID: String = ""
    @TaskLocal static var isDebugMode: Bool = false
}

enum UserContext {
    @TaskLocal static var userID: String? = nil
    @TaskLocal static var locale: Locale = .current
}

// Menggunakan TaskLocal: withValue membuat scope
func handleAPIRequest(traceID: String, userID: String) async throws -> Response {
    try await Tracing.$traceID.withValue(traceID) {
        try await UserContext.$userID.withValue(userID) {
            try await processRequest()
        }
    }
}

// Di mana saja di call chain, bisa baca langsung
func processRequest() async throws -> Response {
    let trace = Tracing.traceID          // otomatis tersedia
    let user = UserContext.userID        // otomatis tersedia

    logger.info("Processing request", metadata: [
        "traceID": "\(trace)",
        "userID": "\(user ?? "anonymous")"
    ])

    return try await fetchDataFromDB()
}

func fetchDataFromDB() async throws -> Response {
    // Masih bisa akses — di-propagate ke seluruh subtask tree
    let trace = Tracing.traceID
    return Response()
}

Propagasi ke Child Tasks

TaskLocal otomatis di-copy ke child tasks yang dibuat dengan Task {}, async let, dan TaskGroup:

swift
func demonstrateTaskLocalPropagation() async {
    await Tracing.$traceID.withValue("req-123") {
        print(Tracing.traceID)  // "req-123"

        // async let: child task juga dapat "req-123"
        async let result1 = Task {
            print(Tracing.traceID)  // "req-123" — di-copy
        }.value

        // TaskGroup: semua child tasks dapat "req-123"
        await withTaskGroup(of: Void.self) { group in
            group.addTask {
                print(Tracing.traceID)  // "req-123"
            }
        }

        // Unstructured Task: TIDAK propagate (berbeda lifecycle)
        Task {
            print(Tracing.traceID)  // "req-123" — TAPI ini Task detached-like
            // Structured Task detached: "" (default value)
        }

        await result1
    }
    print(Tracing.traceID)  // "" — kembali ke default setelah scope withValue
}

Nesting TaskLocal

swift
// Nesting: nilai inner scope override nilai outer scope
await Tracing.$traceID.withValue("parent-trace") {
    print(Tracing.traceID)  // "parent-trace"

    await Tracing.$traceID.withValue("child-trace") {
        print(Tracing.traceID)  // "child-trace"
    }

    print(Tracing.traceID)  // "parent-trace" — dikembalikan otomatis
}

TaskLocal untuk Dependency Injection

swift
// Gunakan TaskLocal untuk DI di tests vs production
protocol DatabaseClient {
    func fetchUser(id: String) async throws -> User
}

enum Dependencies {
    @TaskLocal static var database: any DatabaseClient = ProductionDatabase()
    @TaskLocal static var analytics: AnalyticsService = ProductionAnalytics()
}

// Production: default value dipakai
func fetchUserProfile(id: String) async throws -> UserProfile {
    let user = try await Dependencies.database.fetchUser(id: id)
    return UserProfile(user: user)
}

// Testing: inject mock via TaskLocal
@Test("Profile fetch mengembalikan data yang benar")
func testFetchUserProfile() async throws {
    let mockDB = MockDatabase(users: [User(id: "1", name: "Test")])

    try await Dependencies.$database.withValue(mockDB) {
        let profile = try await fetchUserProfile(id: "1")
        #expect(profile.name == "Test")
    }
}

struct User { let id: String; let name: String }
struct UserProfile { let name: String; init(user: User) { name = user.name } }
class MockDatabase: DatabaseClient {
    let users: [User]
    init(users: [User]) { self.users = users }
    func fetchUser(id: String) async throws -> User {
        guard let user = users.first(where: { $0.id == id }) else { throw DBError.notFound }
        return user
    }
}
class ProductionDatabase: DatabaseClient {
    func fetchUser(id: String) async throws -> User { fatalError("Not in test") }
}
class AnalyticsService {}
class ProductionAnalytics: AnalyticsService {}
enum DBError: Error { case notFound }
struct Response {}