🖤 Sable is a Swift 6+ framework that provides a comprehensive foundation for building reactive, event-driven systems. It prioritizes beautiful API design with natural language interfaces, handling complexity internally while presenting simplicity externally. Its only requirements are Swift 6+ and Foundation, making it widely compatible across Swift environments.
At its core, Sable offers event messaging primitives along with reactive programming patterns for creating resilient, responsive applications. The framework focuses on type safety, thread safety, and expressive APIs that make your code both functional and beautiful.
Sable began as a personal project born from practical needs during app development. Its design choices reflect a belief that code can be both functional and beautiful. That the composition of an API can sing to the reader, similar to how well-crafted prose resonates beyond mere function. Further, writing code should feel natural and expressive - APIs should help an engineer's intent flow easily from their intention and vision.
Some choices, like the use of snake_case and natural language interfaces, stem from accessibility needs that make code more readable and reduce cognitive load for me; and potentially others with similar processing styles. Other choices come from a desire to create APIs that flow naturally when read, making code more approachable and self-documenting.
While Sable intentionally breaks from some Swift community conventions, these departures aren't arbitrary. Each divergence serves a purpose: enhancing readability, reducing cognitive load, or enabling more expressive code. Sable is primarily built for my needs, but I'm sharing it because these approaches might resonate with others too.
Creating interfaces that read like English makes code intent clearer and reduces cognitive load when
reading. Methods like .echoes(original_pulse)
instead of more technical alternatives transform
functional code into readable statements that express intent clearly.
Simple things should remain simple. Complexity is opt-in, revealed only when needed. Basic use cases have straightforward APIs, while power users can access advanced functionality through explicit opt-in.
Meeting design goals takes precedence over community conventions. When established patterns conflict with readability or ergonomics, Sable prioritizes the developer experience.
Type safety isn't merely a goal but a fundamental principle. Sable avoids type erasure entirely, maintaining strong compile-time type safety throughout. This approach not only prevents runtime errors but enables compiler optimizations and provides better tooling support.
Built exclusively on Swift's actor system and structured concurrency model, Sable contains no locks or older threading mechanisms. Every API is designed for heavy actor-based, multi-threaded environments, with proper isolation and communication patterns built-in from the ground up.
All APIs take memory safety seriously by design. When memory management can't be completely hidden from users, the libraries provide clear guidance and tools to ensure proper resource lifecycle management.
Sable contains zero throwable functions, instead embracing the Result type and proper error handling throughout. This design choice leads to more predictable code paths and explicit error handling. The library also avoids exposing functionality that would encourage throwable code in consumer applications.
Components and APIs are designed with clear, specific intents. This principle guides the architecture toward precise, purposeful connections rather than generic, catch-all solutions.
- Type-Safe Messaging Primitives: Full generic type support without any type erasure
- Actor-Ready Value Types: Designed specifically for Swift's actor system
- Natural Language API: Expressive, readable method names that form cohesive sentences
- Immutable Message Design: All operations produce new message instances
- Comprehensive Metadata: Rich operational context that travels with messages
- Tracing & Causality: Built-in support for tracing message flows and causality chains
- Memory Safety: Designed with proper resource lifecycle management
- Debugging Support: Debug-specific features to enhance visibility during development
- Typed Message Handling: Strongly-typed channels for processing pulses with guaranteed delivery
- Actor-Isolated Channels: Ownership model ensuring proper resource lifecycle management
- Priority-Based Processing: Task priority inheritance for appropriate message handling
- Safe Error Handling: Result-based error handling without throwing functions
Sable provides immutable, strongly-typed message primitives called "pulses" that can safely traverse actor boundaries:
// Create a strongly-typed event message
let login_event = UserLoggedIn(user_id: user.id, timestamp: Date())
let pulse = Pulse(login_event)
.priority(.high)
.tagged("auth", "security")
.from(auth_service)
Each pulse contains:
- A unique identifier
- Creation timestamp
- Strongly-typed payload data
- System metadata
Sable uses a fluent builder pattern for creating and modifying pulses, providing a natural language interface that maintains immutability:
// Create an enhanced pulse with rich metadata
let enhanced_pulse = Pulse(login_event)
.debug() // Mark for debugging
.priority(.high) // Set processing priority
.tagged("auth", "security") // Add categorization tags
.from(auth_service) // Identify the source component
// Create a response that maintains the causal chain
let response_pulse = Pulse(AuthenticationCompleted(success: true))
.echoes(enhanced_pulse) // Establish causal relationship
Built-in support for tracing message flows and establishing causal relationships between messages:
// Original pulse with debug enabled
let original = Pulse(StartOperation(name: "sync"))
.debug() // Enable debug tracing
.from(sync_controller) // Set source for context
// First handler creates a response in the chain
let second = Pulse.Respond(
to: original,
with: PrepareData(items: 42),
from: data_service
)
// Second handler continues the chain
let third = Pulse.Respond(
to: second,
with: UploadStarted(batch_id: UUID()),
from: network_service
)
// All pulses share the same trace ID but form a causal chain
// original.meta.trace == second.meta.trace == third.meta.trace
// third.meta.echoes?.id == second.id
// second.meta.echoes?.id == original.id
Sable's Channel system provides a safe, actor-isolated mechanism for delivering strongly-typed pulse messages to registered handlers:
// Create a channel with a generated key
let (auth_channel, auth_key) = Channel.Create { pulse in
await process_auth_event(pulse.data)
}
// Create a channel owned by a specific component
let profile_channel = Channel(owner: profile_service) { pulse in
await update_user_profile(pulse.data)
}
// Send a pulse to a channel
let result = await profile_channel.send(profile_update_pulse)
// Release a channel when no longer needed
await auth_channel.release(key: auth_key)
Channels implement an ownership model where only the component that created the channel can release it:
// Attempt to release with the wrong key
switch await channel.release(key: wrong_key) {
case .success:
// Will never reach here with wrong key
break
case .failure(.invalid(let key)):
// Handle invalid key error
log_security_event("Invalid key used: \(key)")
case .failure(.released):
// Handle already released error
log_warning("Channel was already released")
}
Rich operational context that travels with messages, enabling:
- Operational tracing for debugging complex message flows
- Priority-based scheduling in async contexts
- Causal chain tracking to understand message relationships
- Filtering and routing based on tags
Sable provides a cohesive, unified framework for reactive, event-driven systems:
- Core Messaging Primitives: The Pulse system provides the fundamental message types and metadata handling.
- Channel System: Strongly-typed message handlers with guaranteed delivery and ownership management.
- Transmission Primitives: Advanced message routing, filtering, and processing across actor boundaries.
- Obsidian: Swift extensions and utilities that enhance the standard library with natural language alternatives (formerly SableFoundation)
Add Sable to your Swift package dependencies:
dependencies: [
.package(url: "https://github.com/beeauvin/Sable.git", from: "0.3.0")
]
Import Sable and start building:
import Sable // Imports the core framework
// Define message types
struct UserLoggedIn: Pulsable {
let user_id: UUID
let timestamp: Date
}
struct AuthenticationCompleted: Pulsable {
let success: Bool
let user_id: UUID
}
// Create and customize a pulse
let login_pulse = Pulse(UserLoggedIn(user_id: user.id, timestamp: Date()))
.priority(.high)
.tagged("auth", "security")
// Create a response pulse
let auth_result = Pulse.Respond(
to: login_pulse,
with: AuthenticationCompleted(success: true, user_id: user.id)
)
Sable provides the message primitives and reactive framework, but how you integrate them into your application is entirely up to you:
Use Sable pulses as the foundation for event-driven systems:
// Create an event emitter for authentication events
let auth_emitter = PulseEmitter(source: auth_service)
// Emit events through the emitter
auth_emitter.emit(UserLoggedIn(user_id: user.id, timestamp: Date()))
.priority(.high)
.tagged("auth", "security")
Sable is designed to work seamlessly with Swift's actor system:
actor AuthenticationService {
let emitter: PulseEmitter
init(emitter: PulseEmitter) {
self.emitter = emitter
}
func authenticate(credentials: Credentials) async -> AuthResult {
// Process authentication
let result = // ... authentication logic
// Emit a pulse with the result
emitter.emit(AuthenticationCompleted(success: result.success, user_id: result.user_id))
.priority(.high)
.tagged("auth", "security")
return result
}
}
Implement dedicated message handlers using the Channel system:
// Define a service that processes specific message types
actor NotificationService {
// Store channels as properties
private let event_channel: Channel<SystemEvent>
private let release_key: UUID
init() {
// Create a channel with a generated key
let (channel, key) = Channel.Create { pulse in
await self.process_event(pulse.data)
}
self.event_channel = channel
self.release_key = key
}
// Method to process events internally
private func process_event(_ event: SystemEvent) async {
// Process the event...
}
// Provide the channel to other components
func provide_channel() -> Channel<SystemEvent> {
return event_channel
}
// Clean up resources when shutting down
func shutdown() async {
await event_channel.release(key: release_key)
}
}
// In another component, use the service's channel
actor EventCoordinator {
let notification_channel: Channel<SystemEvent>
init(notification_service: NotificationService) {
self.notification_channel = notification_service.provide_channel()
}
func handle_system_event(_ event: SystemEvent) async {
// Create and send a pulse through the channel
let pulse = Pulse(event)
.priority(.high)
.tagged("system", "notification")
await notification_channel.send(pulse)
}
}
🖤 Sable is available under the Mozilla Public License 2.0.
A copy of the MPLv2 is included license.md file for convenience.
While Sable is primarily developed for personal use, contributions are welcome. I do not have a formal process for this in place at the moment but intend to adopt the Contributor Covenant so those standards are the expectation.
The key consideration for contributions is alignment with Sable's core philosophy and design goals. Technical improvements, documentation, and testing are all valuable areas for contribution. See contributing.md for more details.