Sable

0.3.0

🖤 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.
beeauvin/Sable

What's New

v0.3.0

2025-04-23T18:16:20Z

Features, Fixes & Enhancements

  • Feature: Channel and it's supporting types has been added as a 1:1, thread and type safe, single pulse type, pulse delivery mechanism. Channel uses pulse priority and fire-and-forget task mechanisms to deliver pulses.
  • Feature: Specify currently supported Apple platform versions in the package. It is very likely that Sable could be compiled and ran on much older targets but I have no real way of verifying that. Marking current, non-end-of-life versions gets closer to officially supporting other platforms.

Prose

Channels are the fundamental building block for the rest of Sable's pulse delivery systems. Everything else uses a chain-of-trust method of building with Channels to make more interesting features. Still, they're usable on their own as lightweight pulse delivery mechanisms - or to build other primitives. Channels give a contract that pulses sent to an un-released channel will be delivered to it's handler. There's little magic or special functionality. 🖤

What's Changed

Full Changelog: v0.2.0...v0.3.0

🖤 Sable

tests codecov

🖤 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.

Philosophy & Vision

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.

Design Principles

Natural Language APIs

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.

Progressive Disclosure

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.

Goal-Oriented Design

Meeting design goals takes precedence over community conventions. When established patterns conflict with readability or ergonomics, Sable prioritizes the developer experience.

Comprehensive Type Safety

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.

Modern Thread Safety

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.

Memory Safety

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.

Non-Throwing Code Design

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.

Intentional Specificity

Components and APIs are designed with clear, specific intents. This principle guides the architecture toward precise, purposeful connections rather than generic, catch-all solutions.

Core Features

  • 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

Type-Safe Messaging

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

Fluent Builder Pattern

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

Message Tracing & Causality

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

Typed Channel System

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")
}

Metadata & Context

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

Framework Structure

Sable provides a cohesive, unified framework for reactive, event-driven systems:

Current Components

  • 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.

Coming Soon

  • Transmission Primitives: Advanced message routing, filtering, and processing across actor boundaries.

Related Projects

  • Obsidian: Swift extensions and utilities that enhance the standard library with natural language alternatives (formerly SableFoundation)

Getting Started

Installation

Add Sable to your Swift package dependencies:

dependencies: [
    .package(url: "https://github.com/beeauvin/Sable.git", from: "0.3.0")
]

Basic Usage

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)
)

Integration Patterns

Sable provides the message primitives and reactive framework, but how you integrate them into your application is entirely up to you:

Event-Driven Architecture

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")

Actor-Based Systems

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
    }
}

Channel-Based Message Handling

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)
    }
}

License

🖤 Sable is available under the Mozilla Public License 2.0.

A copy of the MPLv2 is included license.md file for convenience.

Contributing

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.

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

Last updated: Mon May 12 2025 01:11:47 GMT-0900 (Hawaii-Aleutian Daylight Time)