k-arindam/initializable
Stop scattering guard isReady checks everywhere. Let the Swift compiler enforce initialization gates for you using the power of Macros.
π 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
- Non-Throwing Initialization - Throwing Initialization (Failable) - Manual Per-Method Control - Opting Out with @SkipInit
- File Map
π 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.1.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| 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. | | π« Opt-Out Macro (@SkipInit) | Excludes a specific async method from automatic @WaitForInit stamping. |
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| I{"Has @SkipInit?"}
I -->|Yes| J["Skip (opted out) π«"]
I -->|No| H["Stamp @WaitForInit β
"]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 immediately2. 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() β suspendsState Stickiness: The first call to
markInitialized()ormarkFailed(_:)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()
}
}4. Opting Out with @SkipInit
When using @AutoAwaitInit or @AutoAwaitThrowingInit, every async method gets gated automatically. But sometimes you need a specific method to run before initialization completes β for example, the setup method itself, a cancellation handler, or a status check.
Mark those methods with @SkipInit to exclude them:
import Initializable
@AutoAwaitInit
actor NetworkService: Initializable {
let gate = InitializationGate()
private var session: URLSession?
// π« Opted out β this IS the initialization method
@SkipInit
func bootstrap() async {
session = await createSession()
await markInitialized()
}
// π« Opted out β must be callable anytime
@SkipInit
func status() async -> String {
return await initialized ? "ready" : "startingβ¦"
}
// β
Auto-gated β waits for bootstrap()
func fetch(_ url: URL) async -> Data { ... }
func upload(_ data: Data) async { ... }
}[!TIP]
@SkipInitonly works inside types that have@AutoAwaitInitor@AutoAwaitThrowingInit. Applying it elsewhere emits a compiler error with a fix-it.
π 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:#fffFile Map
Sources/
βββ Initializable/ # Public API
β βββ Enums.swift # InitializationState, GateType
β βββ Gate.swift # InitializationGate, ThrowingInitializationGate
β βββ Initializable.swift # Initializable, ThrowingInitializable protocols
β βββ Macros.swift # @AutoAwaitInit, @WaitForInit, @SkipInit 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
βββ SkipInitMacro.swift # @SkipInit peer macro implementation
βββ Messages.swift # Diagnostic & fix-it messages
βββ FunctionDeclSyntax+Extensions.swift # AST inspection helpers
βββ MemberAttributeMacro+Extensions.swift # Duplicate & @SkipInit 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
βββ SkipInitMacroTests.swift # @SkipInit peer macro 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, @SkipInit 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 |
@SkipInit
| Feature | Details | |:---|:---| | Type | @attached(peer) | | Target | Individual async function inside a type using @AutoAwaitInit or @AutoAwaitThrowingInit | | Effect | Prevents the enclosing member-attribute macro from stamping @WaitForInit/@WaitForThrowingInit on this method | | Produces | No peer declarations β acts purely as a compile-time marker |
π 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 |
@SkipInit
| Scenario | Diagnostic Error Message | Xcode Fix-It Suggestion | |:---|:---|:---| | Sync function | @SkipInit can only be applied to async functions, sync functions are never wrapped | Remove @SkipInit | | No enclosing @AutoAwait* | @SkipInit can only be used inside a type marked with @AutoAwaitInit or @AutoAwaitThrowingInit | Remove @SkipInit |
π API Reference
Protocols
Initializable
public protocol Initializable {
var gate: InitializationGate { get }
}initialized: Async boolean property. ReturnstrueaftermarkInitialized().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. Returnstrueonly 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
CancellationErrorautomatically 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.1.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 andCheckedContinuationisolation.
π License
This project is available under the MIT License. See the LICENSE file for details.
Built with β€οΈ using Swift Macros
Package Metadata
Repository: k-arindam/initializable
Default branch: main
README: README.md