Skip to content

k-arindam/Initializable

Repository files navigation

⚑ Initializable

Zero-Boilerplate Async Initialization Gating for Swift Actors, Classes, and Structs

Swift 6.3 Platforms SPM Compatible License: MIT

Stop scattering guard isReady checks everywhere. Let the Swift compiler enforce initialization gates for you using the power of Macros.

Note

Initializable guarantees that your type's async methods will automatically suspend until asynchronous setup is completely finished. No more runtime crashes due to uninitialized state. It works seamlessly with actor, class, and struct.


πŸ›‘ The Problem

Types like actors or classes often require asynchronous setup before they are ready to be usedβ€”connecting to a database, loading a configuration file, or authenticating with a remote server.

Every method that depends on this setup must somehow wait until it's done.

The Tedious Way (Without Initializable)

class DatabaseService {
    private var isReady = false

    func query(_ sql: String) async -> [Row] {
        // 😩 You have to remember this everywhere
        while !isReady { await Task.yield() }
        return try await db.execute(sql)
    }

    func insert(_ row: Row) async {
        // 😩 Miss one and you get a runtime crash
        while !isReady { await Task.yield() }
        db.insert(row)
    }
}

This is tedious, highly error-prone, and doesn't scale as your codebase grows.

✨ The Solution

Initializable gives you a single, elegant annotation on your type. Every async method automatically waits for initialization to complete!

The Elegant Way (With Initializable)

@AutoAwaitInit
class DatabaseService: Initializable {
    let gate = InitializationGate()

    func setup() async {
        await connectToDatabase()
        
        // πŸ”“ Gate opens β€” all waiting methods proceed!
        await markInitialized()  
    }

    // βœ… Automatically waits for setup() β€” ZERO boilerplate needed!
    func query(_ sql: String) async -> [Row] { ... }
    func insert(_ row: Row) async { ... }
    func delete(_ id: Int) async { ... }
}

Tip

Zero runtime overhead after initialization. Zero boilerplate. Zero chance of forgetting a check.


πŸ“– Table of Contents


πŸš€ Quick Start

1. Add the Package

In your Package.swift, add the dependency:

dependencies: [
    .package(url: "https://github.com/k-arindam/Initializable.git", from: "1.0.0")
]

2. Import & Conform

Import the module and annotate your type (can be actor, class, or struct):

import Initializable

@AutoAwaitInit
actor MyService: Initializable {
    let gate = InitializationGate()

    func setup() async {
        // ... perform your async setup ...
        await markInitialized()
    }

    func fetchData() async -> Data {
        // ✨ MAGIC: `await awaitInitialized()` is injected here by the macro
        return cachedData
    }
}

3. Use It

Simply call your methods. They will automatically wait if setup isn't finished!

let service = MyService()

// Kick off the setup (it will run concurrently)
Task { await service.setup() }

// This call will safely suspend until `setup()` completes!
let data = await service.fetchData()  

🧠 Core Concepts

At a high level, Initializable uses Swift Macros to inject gating logic at compile time, and an Actor-based state machine to manage continuations at runtime.

graph LR
    subgraph "πŸ›  Compile Time"
        A["@AutoAwaitInit"] -->|stamps| B["@WaitForInit"]
        B -->|injects| C["await awaitInitialized()"]
    end

    subgraph "πŸƒβ€β™‚οΈ Runtime"
        D["InitializationGate"] -->|pending| E["Callers suspend"]
        D -->|markInitialized| F["Callers resume"]
    end

    C -.->|calls| D
Loading
Concept Description
πŸ“œ Protocol (Initializable) Requires a gate property. Provides markInitialized(), awaitInitialized(), and initialized.
🚧 Gate (InitializationGate) Actor that safely holds continuations and resumes them when the gate opens.
πŸ’‰ Body Macro (@WaitForInit) Injects await awaitInitialized() at the start of a single specific method.
🏷️ Member Macro (@AutoAwaitInit) Automatically stamps @WaitForInit on all async methods in the type.

Variants

There are throwing variants of each component for failable initialization (e.g. network requests that might fail):

Component Type Non-Throwing Throwing (Failable)
Protocol Initializable ThrowingInitializable
Gate InitializationGate ThrowingInitializationGate
Body Macro @WaitForInit @WaitForThrowingInit
Member Macro @AutoAwaitInit @AutoAwaitThrowingInit

πŸ“š Usage Guide

1. Non-Throwing Initialization

Use this when your setup cannot fail (e.g., loading a local cache, connecting to an in-memory store).

import Initializable

@AutoAwaitInit
class CacheService: Initializable {
    let gate = InitializationGate()
    private var store: [String: Data] = [:]

    func warmUp() async {
        store = await loadFromDisk()
        await markInitialized()
    }

    // βœ… Auto-gated β€” automatically waits for warmUp()
    func get(_ key: String) async -> Data? {
        return store[key]
    }

    // ❌ Sync β€” skipped by the macro (no gate needed)
    func cacheDirectory() -> URL {
        FileManager.default.temporaryDirectory
    }
}

View Compile-Time Expansion Flow

flowchart TD
    A["@AutoAwaitInit scans members"] --> B{"Is it a function?"}
    B -->|No| C["Skip (property/init)"]
    B -->|Yes| D{"Is it async?"}
    D -->|No| E["Skip (sync method)"]
    D -->|Yes| F{"Is it a protocol method?"}
    F -->|"markInitialized / awaitInitialized"| G["Skip (excluded)"]
    F -->|No| H["Stamp @WaitForInit βœ…"]
Loading

What Happens at Runtime

sequenceDiagram
    participant Caller1
    participant Caller2
    participant Service
    participant Gate

    Caller1->>Service: get("key")
    Service->>Gate: awaitInitialized()
    Note over Gate: State: pending β†’ suspend

    Caller2->>Service: set("key", data)
    Service->>Gate: awaitInitialized()
    Note over Gate: State: pending β†’ suspend

    Service->>Gate: markInitialized()
    Note over Gate: State: initialized

    Gate-->>Caller1: resume βœ…
    Gate-->>Caller2: resume βœ…

    Note over Gate: Future calls return immediately
Loading

2. Throwing Initialization (Failable)

Use this when your setup can fail (e.g., network connections, database migrations, API authentication).

Important

You must use ThrowingInitializable, ThrowingInitializationGate, and @AutoAwaitThrowingInit.

import Initializable

@AutoAwaitThrowingInit
struct DatabaseService: ThrowingInitializable {
    let gate = ThrowingInitializationGate()
    private var connection: DBConnection?

    mutating func connect(to url: URL) async {
        do {
            connection = try await DBConnection.open(url)
            await markInitialized()    // βœ… Success
        } catch {
            await markFailed(error)    // ❌ Propagate error to all waiting methods
        }
    }

    // βœ… Auto-gated β€” waits for connect, or throws if connect failed
    func query(_ sql: String) async throws -> [Row] {
        return try await connection!.execute(sql)
    }

    // ⚠️ WARNING: If a method is async but NOT throws, the macro will emit a compiler diagnostic with a fix-it!
    // func ping() async -> Bool { ... }
}

View Throwing State Machine

stateDiagram-v2
    [*] --> Pending
    Pending --> Initialized : markInitialized()
    Pending --> Failed : markFailed(error)

    Initialized --> Initialized : markInitialized() [no-op]
    Initialized --> Initialized : markFailed() [no-op]

    Failed --> Failed : markFailed() [no-op]
    Failed --> Failed : markInitialized() [no-op]

    note right of Initialized : awaitInitialized() β†’ returns immediately
    note right of Failed : awaitInitialized() β†’ throws stored error
    note right of Pending : awaitInitialized() β†’ suspends
Loading

State Stickiness: The first call to markInitialized() or markFailed(_:) wins. Subsequent calls to either method are safe no-ops.

3. Manual Per-Method Control

If you prefer fine-grained control instead of the blanket @AutoAwaitInit macro, you can apply @WaitForInit to individual methods manually:

actor SelectiveService: Initializable {
    let gate = InitializationGate()

    func setup() async { await markInitialized() }

    @WaitForInit  // ← Only this method will wait
    func criticalOperation() async -> Result {
        return performWork()
    }

    // No macro β€” caller is entirely responsible for timing
    func bestEffortOperation() async -> Result? {
        return try? performWork()
    }
}

πŸ— Architecture

Initializable is split into the runtime library and the compile-time macro plugin.

graph TB
    subgraph "Your App"
        App["App Code"]
    end

    subgraph "πŸ“¦ Initializable Module"
        Proto["Initializable Protocol<br/>ThrowingInitializable Protocol"]
        Gate["InitializationGate<br/>ThrowingInitializationGate"]
        Macros["Macro Declarations<br/>@AutoAwaitInit, @WaitForInit, etc."]
    end

    subgraph "πŸ”Œ InitializableMacros Module (Compiler Plugin)"
        MacroImpl["AutoAwaitInitMacro<br/>WaitForInitMacro"]
        Diag["Diagnostics & Fix-Its"]
        Helpers["Syntax Helpers"]
    end

    App -->|"import Initializable"| Proto
    App -->|"uses"| Gate
    App -->|"@AutoAwaitInit"| Macros
    Macros -->|"#externalMacro"| MacroImpl
    MacroImpl --> Diag
    MacroImpl --> Helpers

    style App fill:#2d2d2d,stroke:#888,color:#fff
    style Proto fill:#1a5276,stroke:#2980b9,color:#fff
    style Gate fill:#1a5276,stroke:#2980b9,color:#fff
    style Macros fill:#1a5276,stroke:#2980b9,color:#fff
    style MacroImpl fill:#4a235a,stroke:#8e44ad,color:#fff
    style Diag fill:#4a235a,stroke:#8e44ad,color:#fff
    style Helpers fill:#4a235a,stroke:#8e44ad,color:#fff
Loading

File Map

Sources/
β”œβ”€β”€ Initializable/                         # Public API
β”‚   β”œβ”€β”€ Enums.swift                        # InitializationState, GateType
β”‚   β”œβ”€β”€ Gate.swift                         # InitializationGate, ThrowingInitializationGate
β”‚   β”œβ”€β”€ Initializable.swift                # Initializable, ThrowingInitializable protocols
β”‚   └── Macros.swift                       # @AutoAwaitInit, @WaitForInit declarations
β”‚
└── InitializableMacros/                   # Compiler plugin (not shipped in binary)
    β”œβ”€β”€ InitializableMacros.swift           # @main plugin entry point
    β”œβ”€β”€ AutoAwaitInitMacro.swift            # Member-attribute macro implementations
    β”œβ”€β”€ WaitForInitMacro.swift              # Body macro implementations
    β”œβ”€β”€ Messages.swift                      # Diagnostic & fix-it messages
    β”œβ”€β”€ FunctionDeclSyntax+Extensions.swift # AST inspection helpers
    └── MemberAttributeMacro+Extensions.swift # Duplicate detection logic

Tests/
└── InitializableTests/
    β”œβ”€β”€ WaitForInitMacroTests.swift         # @WaitForInit body macro tests
    β”œβ”€β”€ WaitForThrowingInitMacroTests.swift  # @WaitForThrowingInit body macro tests
    β”œβ”€β”€ AutoAwaitInitMacroTests.swift        # @AutoAwaitInit member-attribute tests
    β”œβ”€β”€ AutoAwaitThrowingInitMacroTests.swift # @AutoAwaitThrowingInit tests
    └── RuntimeTests.swift                   # Gate & protocol runtime behavior tests

πŸ” Macro Reference

@AutoAwaitInit & @AutoAwaitThrowingInit

Feature Details
Type @attached(memberAttribute)
Target Actor / Class / Struct conforming to Initializable (or ThrowingInitializable)
Effect Stamps @WaitForInit (or @WaitForThrowingInit) on every qualifying async method
Excludes markInitialized(), awaitInitialized(), markFailed(), non-function members, sync methods

@WaitForInit & @WaitForThrowingInit

Feature Details
Type @attached(body)
Target Individual async (or async throws) function inside a conforming type
Effect Prepends await awaitInitialized() (or try await...) to the function body

πŸ›  Diagnostics & Fix-Its

Initializable provides rich compiler diagnostics with actionable fix-its. You'll never be left guessing what went wrong!

@WaitForInit & @WaitForThrowingInit

Scenario Diagnostic Error Message Xcode Fix-It Suggestion
Sync function @WaitForInit requires the function to be 'async' Add async
throws-only function @WaitForThrowingInit requires the function to be 'async' Add async
async-only function @WaitForThrowingInit requires the function to be 'throws' Add throws
Sync non-throwing @WaitForThrowingInit requires the function to be 'async throws' Add async throws
No conformance @WaitForInit can only be used in a type that conforms to 'Initializable' None
Free function @WaitForInit can only be applied inside a type declaration None

@AutoAwaitInit & @AutoAwaitThrowingInit

Scenario Diagnostic Error Message Xcode Fix-It Suggestion
No conformance @AutoAwaitInit can only be applied to a type that conforms to 'Initializable' None
Duplicate attribute @WaitForInit should not be added manually when @AutoAwaitInit is applied... Remove @WaitForInit

πŸ“– API Reference

Protocols

Initializable

public protocol Initializable {
    var gate: InitializationGate { get }
}
  • initialized: Async boolean property. Returns true after markInitialized().
  • markInitialized(): Opens the gate. Safe to call multiple times (idempotent).
  • awaitInitialized(): Suspends execution until the gate is opened.

ThrowingInitializable

public protocol ThrowingInitializable {
    var gate: ThrowingInitializationGate { get }
}
  • initialized: Async boolean property. Returns true only on success.
  • markFailed<E: Error>(_ error: E): Fails the gate with the given error. Idempotent.
  • awaitInitialized() throws: Suspends until resolved; throws if initialization failed.

Gates

InitializationGate

  • Continuation type: CheckedContinuation<Void, Never>
  • Cancellation: Resumes normally (returns Void). Task cancellation will not throw.
  • Thread safety: Actor-isolated β€” all state mutations are serial.

ThrowingInitializationGate

  • Continuation type: CheckedContinuation<Void, any Error>
  • Cancellation: Throws CancellationError automatically if the waiting task is cancelled.
  • State stickiness: The first resolution (success or failure) permanently locks the state.

πŸ“¦ Installation

Swift Package Manager

Add the dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/k-arindam/Initializable.git", from: "1.0.0")
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: ["Initializable"]
    )
]

Or via Xcode: File β†’ Add Package Dependencies β†’ paste the repository URL.


βš™οΈ Requirements

Platform/Tool Minimum Version
Swift 6.3
Xcode 16.3
iOS 15.0
macOS 12.0
tvOS 15.0
watchOS 9.0

Note

Swift macros generally require Swift 5.9+, but this package leverages advanced Swift 6.3 features including @attached(body) macros and CheckedContinuation isolation.


πŸ“„ License

This project is available under the MIT License. See the LICENSE file for details.

Built with ❀️ using Swift Macros

About

Zero-Boilerplate Async Initialization Gating for Swift Actors & Classes

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages