{"id":29089985,"url":"https://github.com/customerio/safe-journey","last_synced_at":"2025-10-25T01:14:46.841Z","repository":{"id":301112223,"uuid":"1007284743","full_name":"customerio/safe-journey","owner":"customerio","description":"SafeJourney makes Swift's mutable state visually obvious and statically safe","archived":false,"fork":false,"pushed_at":"2025-06-25T07:00:51.000Z","size":68,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-25T08:19:16.012Z","etag":null,"topics":["concurrency-patterns","static-analysis","swift"],"latest_commit_sha":null,"homepage":"https://customer.io","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/customerio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-23T18:48:22.000Z","updated_at":"2025-06-25T07:00:54.000Z","dependencies_parsed_at":"2025-06-25T08:19:18.042Z","dependency_job_id":"606c81f9-8f52-4b9c-8743-7436cbf8d670","html_url":"https://github.com/customerio/safe-journey","commit_stats":null,"previous_names":["customerio/safe-journey"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/customerio/safe-journey","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/customerio%2Fsafe-journey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/customerio%2Fsafe-journey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/customerio%2Fsafe-journey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/customerio%2Fsafe-journey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/customerio","download_url":"https://codeload.github.com/customerio/safe-journey/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/customerio%2Fsafe-journey/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262371698,"owners_count":23300599,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency-patterns","static-analysis","swift"],"created_at":"2025-06-28T04:06:29.162Z","updated_at":"2025-10-25T01:14:46.723Z","avatar_url":"https://github.com/customerio.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Safe Journey Pattern\n\n\u003e An elegant thread safety pattern for Swift that makes concurrency constraints visible and enforceable through naming conventions and focused static analysis.\n\n[![Swift](https://img.shields.io/badge/Swift-5.5+-orange.svg)](https://swift.org)\n[![Platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS%20%7C%20Linux-blue.svg)](https://swift.org)\n[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n\n## Quick Start\n\n```bash\n# Clone or add SafeJourney as a dependency\ngit clone https://github.com/customerio/safe-journey.git\ncd safe-journey\n\n# Check your Swift project\nswift run sj Sources/\n```\n\n## Table of Contents\n\n- [What is SafeJourney?](#what-is-safejourney)\n- [Why Use This Pattern?](#why-use-this-pattern)\n- [Pattern Rules](#pattern-rules)\n- [Installation](#installation)\n- [Usage](#usage)\n- [Integration with CI/CD](#integration-with-cicd)\n- [Examples](#examples)\n- [FAQ](#faq)\n- [Contributing](#contributing)\n\n## What is SafeJourney?\n\nSafeJourney is a basic thread safety pattern for Swift that uses visual naming conventions to make mutable state visible and provides simple static checks to maintain consistency. Originally developed by Customer.io for their mobile SDKs, it focuses on a specific pattern rather than comprehensive concurrency analysis.\n\n**SafeJourney is not a sophisticated static analyzer** - it's a focused pattern matcher with clear limitations. It works well for teams that adopt the underscore naming convention and want basic guard rails to prevent common mistakes.\n\n### Core Concept\n\nBy marking shared mutable state and its access paths explicitly, SafeJourney makes threading intent visible and verifiable.\n\n```swift\npublic final class EventsProcessor: @unchecked Sendable {\n    private let maxEventsBatchSize: Int\n    private let batchSyncQueue: DispatchQueue\n\n    // ✅ Requires protection\n    private var _eventData: [[String: Any]] = []\n    private var _timerCancellable: AnyCancellable?\n\n    public func enqueue(eventPayload: [String: Any]) throws {\n        async { [weak self] in\n            self?._eventData.append(eventPayload)\n            self?._persistEvents()\n        }\n    }\n\n    private func _persistEvents() {\n        storage.save(_eventData)\n    }\n}\n```\n\n## Why Use This Pattern?\n\n### Thread Safety You Can See\n\nUnderscore-prefixed properties and methods make mutable state and its access constraints immediately visible in code reviews.\n\n### Prevents Deadlocks by Design\n\nUnderscore functions never re-enter queues. Public methods enforce queue protection. This eliminates many common pitfalls in concurrent code.\n\n### Enforceable via Basic Pattern Matching\n\nA simple checker catches violations of the pattern within individual files. It has limitations but provides useful guard rails for teams using this convention.\n\n### Low Friction for Teams\n\nThe pattern is simple to learn, fast to apply, and helps teams avoid subtle concurrency bugs without heavyweight solutions.\n\n### ⚡ Performance-Efficient\n\nDispatchQueue is a performant serial queue. SafeJourney encourages batching and queue-local operations.\n\n## Pattern Rules\n\n### Rule 1: Prefix Mutable State with an Underscore\n\n```swift\n// ❌ Unsafe: the need for protection is not clear\nprivate var mutableProperty: String = \"\"\n\n// ✅ Safe: clearly marked for protected access\nprivate var _mutableProperty: String = \"\"\n```\n\n### Rule 2: Underscore Properties Must Be Private\n\n```swift\n// ❌ Unsafe: exposed mutable state can be misused\npublic var _state: String = \"\"\n\n// ✅ Safe: only accessible within the class\nprivate var _state: String = \"\"\n```\n\n### Rule 3: Public Methods Must Use Queue Protection\n\n```swift\nfunc updateState() {\n    // ❌ Unsafe direct access\n    _mutableProperty = \"new\"\n\n    // ✅ Safe access inside queue\n    queue.sync {\n        _mutableProperty = \"new\"\n    }\n}\n```\n\n### Rule 4: Underscore Methods Must Not Call Non-Underscore Methods\n\n```swift\nprivate func _processData() {\n    // ❌ Unsafe: might cause re-entry or deadlocks\n    publicMethod()\n\n    // ❌ Unsafe: calls to non-underscore methods in same file\n    helperMethod()\n\n    // ✅ Safe: underscore methods can call other underscore methods\n    _state = \"processed\"\n    _helperMethod()\n}\n\n// Note: Safe Journey checker only analyzes functions within the same file.\n// Calls to external functions/frameworks are not analyzed (tool limitation).\n```\n\n## Installation\n\nA convention is great till it fails due to human error. Hence a complementary static check acts as a consistent guard rail.\n\n### Option 1: Clone Repository (Recommended)\n\n```bash\ngit clone https://github.com/customerio/safe-journey.git\ncd safe-journey\nswift run sj\n```\n\n### Option 2: Swift Package Manager\n\nAdd SafeJourney as a dependency in your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/customerio/safe-journey.git\", from: \"1.0.0\")\n]\n```\n\nThen run the checker from your project root:\n\n```bash\nswift run --package-path path/to/safe-journey sj Sources/\n```\n\n## Usage\n\n```bash\nswift run sj           # Current directory\nswift run sj Sources/  # Specific directory\nswift run sj MyFile.swift  # Specific file\nswift run sj --help    # Help menu\n\n# Custom queue wrapper methods\nswift run sj --queue-methods customAsync,safeSync Sources/\n\n# Using configuration file\nswift run sj --config safejourney.json Sources/\n```\n\n### Configuration\n\nCreate a `safejourney.json` file to customize queue wrapper methods:\n\n```json\n{\n  \"queueWrapperMethods\": [\"sync\", \"async\", \"customAsync\", \"safeExecute\"],\n  \"excludePatterns\": [\"Tests\", \"Generated\"]\n}\n```\n\nOr pass custom methods via CLI:\n\n```bash\nswift run sj --queue-methods customAsync,differentAsyncHelper Sources/\n```\n\n### Example Output\n\n````\n🔍 SafeJourney Pattern Checker\n🎯 Checking: Sources/\n\n❌ Sources/EventProcessor.swift:45: Function 'updateState' cannot directly access _eventData. Use queue protection\n   💡 Suggestion: Wrap in queue.sync { } or queue.async { }\n\n⚠️  Sources/UserManager.swift:23: Mutable property should use underscore prefix\n   💡 Suggestion: Change 'var property' to 'private var _property'\n\n📊 Summary: 1 error, 1 warning\n🚨 Fix violations before committing.\n\n## Integration with CI/CD\n\n### GitHub Actions\n\n```yaml\nname: Thread Safety Check\non: [push, pull_request]\n\njobs:\n  safe-journey:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Setup Swift\n        uses: swift-actions/setup-swift@v1\n        with:\n          swift-version: \"5.9\"\n\n      - name: Run Thread Safety Check\n        run: swift run sj Sources/\n```\n\n### Pre-commit Hook\n\n```bash\n#!/bin/sh\n# Ensure SafeJourney is available\nif [ ! -d \".safe-journey\" ]; then\n    echo \"📥 Cloning SafeJourney...\"\n    git clone https://github.com/customerio/safe-journey.git .safe-journey\nfi\n\nswift run --package-path .safe-journey sj Sources/\nif [ $? -ne 0 ]; then\n    echo \"❌ Thread safety violations found. Please fix before committing.\"\n    exit 1\nfi\n```\n\n### Xcode Build Phase\n\n```bash\n# Ensure SafeJourney is available\nif [ ! -d \"${SRCROOT}/.safe-journey\" ]; then\n    echo \"📥 Cloning SafeJourney...\"\n    git clone https://github.com/customerio/safe-journey.git \"${SRCROOT}/.safe-journey\"\nfi\n\n# Run SafeJourney checker\ncd \"${SRCROOT}/.safe-journey\"\nswift run sj \"${SRCROOT}/Sources\"\n```\n\n## Examples\n\nSee the `examples/` directory for complete working examples of the SafeJourney pattern.\n\n## Limitations\n\nSafeJourney is a **basic pattern matcher**, not a comprehensive static analyzer. Here are its intentional limitations:\n\n### ✅ **What SafeJourney Detects**\n- Underscore property access without queue protection\n- Non-private underscore properties and functions\n- Underscore functions calling non-underscore functions **in the same file**\n- Mutable properties without underscore prefix\n\n### ❌ **What SafeJourney Does NOT Detect**\n- Cross-file function calls (calls to external modules/frameworks are ignored)\n- Complex data flow analysis\n- Race conditions beyond the basic pattern\n- Sophisticated concurrency issues\n- System function safety (assumes system calls are safe)\n\n### 🎯 **Design Philosophy**\nSafeJourney prioritizes **simplicity and clarity** over comprehensive analysis. It's designed to catch common violations of a specific naming convention, not to solve all concurrency problems.\n\nIf you need comprehensive static analysis, consider tools like Swift's built-in concurrency checking (`-strict-concurrency=complete`) or more sophisticated analyzers.\n\n## FAQ\n\n### Q: Why not just use actors?\n\nActors are useful in isolation, but in many real-world systems, concurrency is cross-cutting. `await` boundaries introduce partial transaction points, making it hard to reason about state. SafeJourney gives finer control over execution and lets you isolate concerns cleanly.\n\n### Q: Does this impact performance?\n\nNot meaningfully. Serial queues are efficient and widely used in many performant applications.\n\n### Q: How do I migrate existing code?\n\nStart small. Apply the pattern to your most shared or error-prone classes first. Let the checker identify violations incrementally.\n\n### Q: Can underscore methods invoke callbacks?\n\nYes, as long as they escape to another queue.\n\n```swift\nprivate func _process(completion: @escaping () -\u003e Void, callbackQueue: DispatchQueue = .global()) {\n    // work...\n    callbackQueue.async {\n        completion()\n    }\n}\n```\n\n## Contributing\n\nWe welcome contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n```bash\ngit clone https://github.com/customerio/safe-journey.git\ncd safe-journey\n./test.sh\n```\n\n## License\n\nMIT License — see [LICENSE](LICENSE).\n\n## Acknowledgments\n\nDeveloped by Customer.io to solve production-grade concurrency challenges in their SDKs. Special thanks to the Mobile team for pioneering this effort.\n\n---\n\n**Ready to bring clarity and safety to your concurrency model?**\n\nStart with the [Quick Start](#quick-start) guide above!\n````\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcustomerio%2Fsafe-journey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcustomerio%2Fsafe-journey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcustomerio%2Fsafe-journey/lists"}