{"id":18654293,"url":"https://github.com/fatbobman/persistenthistorytrackingkit","last_synced_at":"2026-03-17T15:08:23.977Z","repository":{"id":47511007,"uuid":"459465457","full_name":"fatbobman/PersistentHistoryTrackingKit","owner":"fatbobman","description":"A library for managing Core Data's Persistent History Tracking","archived":false,"fork":false,"pushed_at":"2024-10-30T10:14:06.000Z","size":85,"stargazers_count":103,"open_issues_count":0,"forks_count":13,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-30T05:04:18.958Z","etag":null,"topics":["coredata","persistent-storage"],"latest_commit_sha":null,"homepage":"","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/fatbobman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":null,"patreon":"fatbobman","open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":"fatbobman","thanks_dev":null,"custom":["https://afdian.com","https://www.paypal.com/paypalme/fatbobman"]}},"created_at":"2022-02-15T07:04:34.000Z","updated_at":"2025-03-28T06:27:32.000Z","dependencies_parsed_at":"2024-01-01T03:25:34.611Z","dependency_job_id":"3c08fe5a-0428-476b-8398-c3e1a06a1b05","html_url":"https://github.com/fatbobman/PersistentHistoryTrackingKit","commit_stats":{"total_commits":49,"total_committers":2,"mean_commits":24.5,"dds":"0.020408163265306145","last_synced_commit":"fd6f3ed2bc8f83428cdd5e31ef8313e9af862b07"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatbobman%2FPersistentHistoryTrackingKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatbobman%2FPersistentHistoryTrackingKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatbobman%2FPersistentHistoryTrackingKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatbobman%2FPersistentHistoryTrackingKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fatbobman","download_url":"https://codeload.github.com/fatbobman/PersistentHistoryTrackingKit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247441046,"owners_count":20939239,"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":["coredata","persistent-storage"],"created_at":"2024-11-07T07:14:34.746Z","updated_at":"2026-03-17T15:08:23.968Z","avatar_url":"https://github.com/fatbobman.png","language":"Swift","funding_links":["https://patreon.com/fatbobman","https://buymeacoffee.com/fatbobman","https://afdian.com","https://www.paypal.com/paypalme/fatbobman"],"categories":[],"sub_categories":[],"readme":"# Persistent History Tracking Kit 2\n\n**Swift 6 ready** • **Actor-based** • **Fully concurrent** • **Type-safe**\n\nA modern, production-ready library for handling Core Data's Persistent History Tracking with full Swift 6 concurrency support.\n\n![Platform](https://img.shields.io/badge/Platform-iOS%2013%2B%20%7C%20macOS%2010.15%2B%20%7C%20macCatalyst%2013%2B%20%7C%20tvOS%2013%2B%20%7C%20watchOS%206%2B%20%7C%20visionOS%201%2B-blue)\n![Swift](https://img.shields.io/badge/Swift-6.0-orange)\n![License](https://img.shields.io/badge/License-MIT-green)[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fatbobman/PersistentHistoryTrackingKit)\n\n[English](https://github.com/fatbobman/PersistentHistoryTrackingKit/blob/main/README.md) | [中文版说明](https://github.com/fatbobman/PersistentHistoryTrackingKit/blob/main/READMECN.md)\n\n---\n\n## What's New in V2 🎉\n\nVersion 2 is a **complete rewrite** with modern Swift concurrency:\n\n- ✅ **Full Swift 6 Compliance** - Concurrency-safe design tuned for Swift 6\n- ✅ **Actor-Based Architecture** - Thread-safe by design with `HookRegistryActor` and `TransactionProcessorActor`\n- ✅ **Zero Memory Leaks** - No retain cycles, properly managed lifecycle\n- ✅ **Data Race Free** - Comprehensive concurrency testing with Swift Testing\n- ✅ **Hook System** - Powerful Observer and Merge Hooks for custom behaviors\n- ✅ **Modern API** - Async/await throughout, UUID-based hook management\n\n**Migration from V1:** V2 declares iOS 13+, macOS 10.15+, macCatalyst 13+, tvOS 13+,\nwatchOS 6+, visionOS 1+, and Swift 6. Current runtime validation has been performed on iOS 15+.\nSee the [Migration Guide](Docs/MigrationGuide.md) for migration steps and behavior changes.\n\n---\n\n## What is Persistent History Tracking?\n\n\u003e Use persistent history tracking to determine what changes have occurred in the store since the enabling of persistent history tracking. — Apple Documentation\n\nWhen you enable Persistent History Tracking, Core Data creates **transactions** for all changes across:\n- Your main app\n- App extensions (widgets, share extensions, etc.)\n- Background contexts\n- CloudKit sync (if enabled)\n\n**PersistentHistoryTrackingKit** automates the process of:\n1. 📥 Fetching new transactions from other contexts\n2. 🔄 Merging them into your app's context\n3. 🧹 Cleaning up old transactions\n4. 🎣 Triggering custom hooks for monitoring or custom merge logic\n\n**Want to learn more?**\n\n- 📖 **[Using Persistent History Tracking in CoreData](https://fatbobman.com/en/posts/persistenthistorytracking/)** - Comprehensive guide covering the fundamentals, concepts, and implementation patterns\n\n---\n\n## Version Availability\n\n### V2 (Current Branch)\n\n- **Minimum Requirements**: iOS 13+, macOS 10.15+, macCatalyst 13+, tvOS 13+, watchOS 6+, visionOS 1+, Swift 6.0+\n- **Features**: Actor-based architecture, Hook system, full Swift 6 concurrency\n- **Runtime Validation**: Currently tested on iOS 15+ with current Xcode toolchains\n- **Recommended for**: New projects adopting Swift 6\n\n### V1 (Stable)\n\n- **Minimum Requirements**: iOS 13+, macOS 10.15+, Swift 5.5+\n- **Features**: Proven stability, lower system requirements\n- **Recommended for**: Projects that prefer a pre-Swift-6 toolchain or the battle-tested V1 API\n\n**Use V1 if:**\n\n- You need to stay on a pre-Swift-6 toolchain\n- You're not ready to migrate to Swift 6\n- You prefer the battle-tested V1 API\n\n📦 **Install V1**:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/fatbobman/PersistentHistoryTrackingKit.git\", from: \"1.0.0\")\n]\n```\n\nOr use the `version-1` branch: [V1 Documentation](https://github.com/fatbobman/PersistentHistoryTrackingKit/tree/version-1)\n\nMoving an existing V1 app to V2? Read the [Migration Guide](Docs/MigrationGuide.md).\n\n---\n\n## Quick Start\n\n### Installation\n\nAdd to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/fatbobman/PersistentHistoryTrackingKit.git\", from: \"2.0.0\")\n]\n```\n\n### Basic Setup\n\n```swift\nimport CoreData\nimport PersistentHistoryTrackingKit\n\n// 1. Enable persistent history tracking in your Core Data stack\nlet container = NSPersistentContainer(name: \"MyApp\")\nlet description = container.persistentStoreDescriptions.first!\n\ndescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)\ndescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)\n\ncontainer.loadPersistentStores { _, error in\n    if let error = error {\n        fatalError(\"Failed to load store: \\(error)\")\n    }\n}\n\n// 2. Set transaction authors\ncontainer.viewContext.transactionAuthor = \"MainApp\"\n\n// 3. Initialize PersistentHistoryTrackingKit\nlet kit = PersistentHistoryTrackingKit(\n    container: container,\n    contexts: [container.viewContext],\n    currentAuthor: \"MainApp\",\n    allAuthors: [\"MainApp\", \"WidgetExtension\", \"ShareExtension\"],\n    userDefaults: .standard,\n    cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7), // 7 days\n    logLevel: 1\n)\n\n// Kit starts automatically by default\n```\n\nThat's it! The kit will now automatically:\n- Detect remote changes\n- Merge transactions from other authors\n- Clean up old history\n- Keep your contexts in sync\n\n---\n\n## Core Concepts\n\n### Authors\n\nEach part of your app should have a unique **author** name:\n\n```swift\n// Main app\ncontainer.viewContext.transactionAuthor = \"MainApp\"\n\n// Widget extension\nwidgetContext.transactionAuthor = \"WidgetExtension\"\n\n// Background batch operations\nbatchContext.transactionAuthor = \"BatchProcessor\"\n```\n\nThen configure the kit with all authors:\n\n```swift\nallAuthors: [\"MainApp\", \"WidgetExtension\", \"BatchProcessor\"]\n```\n\n### Cleanup Strategies\n\n**Important**: Transaction cleanup is optional and low-overhead. Old transactions don't impact performance significantly. There's no need for aggressive cleanup - choose a relaxed interval that works for your app.\n\n```swift\n// Option 1: Time-based cleanup (recommended)\n// Clean up at most once per time interval\ncleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7) // 7 days\n\n// Option 2: Notification-based cleanup\n// Clean up after N notifications (less common)\ncleanStrategy: .byNotification(times: 10)\n\n// Option 3: No automatic cleanup (manual control)\ncleanStrategy: .none\n```\n\n**Recommendations**:\n\n- **Most apps**: Use `.byDuration(seconds: 60 * 60 * 24 * 7)` (7 days) - provides a good balance\n- **CloudKit users**: **Must** use `.byDuration(seconds: 60 * 60 * 24 * 7)` or longer to avoid `NSPersistentHistoryTokenExpiredError`\n- **Frequent transactions**: Consider `.byDuration(seconds: 60 * 60 * 24 * 3)` (3 days)\n- **Manual control**: Use `.none` and clean on specific events (app background, etc.)\n\nAutomatic cleanup is conservative: the kit cleans only after every non-batch author has recorded\nits merge timestamp in the shared `UserDefaults` store. If one required author has not merged yet,\nautomatic cleanup is skipped.\n\n**⚠️ Important for CloudKit Users**:\n\nCloudKit relies on persistent history internally. If history is cleaned up too aggressively, CloudKit may lose its tracking tokens, causing `NSPersistentHistoryTokenExpiredError` (error code 134301), which can lead to local database purges and forced re-sync from iCloud.\n\n**Always use time-based cleanup with sufficient duration** (7+ days) when using CloudKit:\n\n```swift\nlet kit = PersistentHistoryTrackingKit(\n    container: container,\n    currentAuthor: \"MainApp\",\n    allAuthors: [\"MainApp\", \"WidgetExtension\"],\n    cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7),  // 7 days minimum for CloudKit\n    userDefaults: userDefaults\n)\n```\n\n**Note**: By default, the kit does **not** clean up transactions generated by `NSPersistentCloudKitContainer` (CloudKit mirroring), avoiding interference with CloudKit's internal synchronization.\n\n### Manual Cleanup\n\nFor maximum flexibility, you can control when cleanup happens:\n\n```swift\nlet kit = PersistentHistoryTrackingKit(\n    // ... other parameters\n    cleanStrategy: .none,  // Disable automatic cleanup\n    autoStart: false\n)\n\n// Build a manual cleaner\nlet cleaner = kit.cleanerBuilder()\n\n// Clean up at your preferred timing\n// For example: when app enters background, during low usage, etc.\nTask {\n    await cleaner.clean()\n}\n\n// Start the kit when ready\nkit.start()\n```\n\n---\n\n## Hook System 🎣\n\nV2 introduces a powerful **Hook System** for monitoring changes and customizing merge behavior.\n\n### Observer Hooks (Read-Only Monitoring)\n\nMonitor specific entity operations without modifying data:\n\n```swift\n// Monitor Person insertions\nlet hookId = await kit.registerObserver(\n    entityName: \"Person\",\n    operation: .insert\n) { contexts in\n    for context in contexts {\n        print(\"New person created: \\(context.objectIDURL)\")\n\n        // Send analytics\n        await Analytics.track(event: \"person_created\", properties: [\n            \"timestamp\": context.timestamp,\n            \"author\": context.author\n        ])\n    }\n}\n\n// Remove specific hook later\nawait kit.removeObserver(id: hookId)\n\n// Or remove all hooks for an entity+operation\nawait kit.removeObserver(entityName: \"Person\", operation: .insert)\n```\n\n**Context batching:** The callback receives `[HookContext]` that groups changes **per transaction + entity + operation**. Insert multiple `Person` objects in one transaction → one callback with an array of contexts.\n\n**Use cases:** Logging, analytics, notifications, cache invalidation\n\n### Merge Hooks (Custom Merge Logic)\n\nImplement custom merge behavior with full access to Core Data:\n\n```swift\n// Custom conflict resolution\nawait kit.registerMergeHook { input in\n    for transaction in input.transactions {\n        for context in input.contexts {\n            await context.perform {\n                // Custom merge logic here\n                // You have full access to NSManagedObjectContext\n            }\n        }\n    }\n\n    // Return .goOn to continue to next hook\n    // Return .finish to skip remaining hooks and default merge\n    return .goOn\n}\n```\n\n**Use cases:** Conflict resolution, deduplication, validation, custom merge strategies\n\n### Real-World Examples\n\nMerge Hooks can customize the merge pipeline itself, for example by temporarily disabling `undoManager` while applying history transactions.\n\n**📚 Complete Hook Documentation:** [Docs/HookMechanism.md](Docs/HookMechanism.md)\n\n---\n\n## API Reference\n\n### Initialization Parameters\n\n| Parameter | Type | Description | Default |\n|-----------|------|-------------|---------|\n| `container` | `NSPersistentContainer` | Your Core Data container | Required |\n| `contexts` | `[NSManagedObjectContext]?` | Contexts to merge into | `[container.viewContext]` |\n| `currentAuthor` | `String` | Current app's author name | Required |\n| `allAuthors` | `[String]` | All author names to track | Required |\n| `includingCloudKitMirroring` | `Bool` | Include CloudKit transactions | `false` |\n| `batchAuthors` | `[String]` | Authors that only write, never merge | `[]` |\n| `userDefaults` | `UserDefaults` | Storage for timestamps | Required |\n| `cleanStrategy` | `TransactionCleanStrategy` | Cleanup strategy | `.none` |\n| `maximumDuration` | `TimeInterval` | Reserved for future cleanup readiness policies | 7 days |\n| `uniqueString` | `String` | UserDefaults key prefix | Auto-generated |\n| `logger` | `PersistentHistoryTrackingKitLoggerProtocol?` | Custom logger | `DefaultLogger` |\n| `logLevel` | `Int` | Log verbosity (0-2) | `1` |\n| `autoStart` | `Bool` | Start automatically | `true` |\n\n### Observer Hook Methods\n\n```swift\n// Register observer hook (returns UUID for removal)\nfunc registerObserver(\n    entityName: String,\n    operation: HookOperation,\n    callback: @escaping HookCallback\n) async -\u003e UUID\n\n// Remove specific hook by UUID\nfunc removeObserver(id: UUID) async -\u003e Bool\n\n// Remove all hooks for entity+operation\nfunc removeObserver(entityName: String, operation: HookOperation) async\n\n// Remove all observer hooks\nfunc removeAllObservers() async\n```\n\n### Merge Hook Methods\n\n```swift\n// Register merge hook (returns UUID)\nfunc registerMergeHook(\n    before hookId: UUID? = nil,\n    callback: @escaping MergeHookCallback\n) async -\u003e UUID\n\n// Remove specific merge hook\nfunc removeMergeHook(id: UUID) async -\u003e Bool\n\n// Remove all merge hooks\nfunc removeAllMergeHooks() async\n```\n\n### Control Methods\n\n```swift\n// Start/stop the kit\nfunc start()\nfunc stop()\n\n// Build a manual cleaner\nfunc cleanerBuilder() -\u003e ManualCleanerActor\n```\n\n---\n\n## Advanced Usage\n\n### App Groups\n\nFor sharing data across app and extensions:\n\n```swift\nlet appGroupDefaults = UserDefaults(suiteName: \"group.com.yourapp\")!\n\nlet kit = PersistentHistoryTrackingKit(\n    container: container,\n    currentAuthor: \"MainApp\",\n    allAuthors: [\"MainApp\", \"WidgetExtension\"],\n    userDefaults: appGroupDefaults, // Use shared UserDefaults\n    cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7)\n)\n```\n\n### Custom Logger\n\nIntegrate with your logging system:\n\n```swift\nstruct MyLogger: PersistentHistoryTrackingKitLoggerProtocol {\n    func log(type: PersistentHistoryTrackingKitLogType, message: String) {\n        switch type {\n        case .debug:\n            Logger.debug(message)\n        case .info:\n            Logger.info(message)\n        case .notice:\n            Logger.notice(message)\n        case .error:\n            Logger.error(message)\n        case .fault:\n            Logger.fault(message)\n        }\n    }\n}\n\nlet kit = PersistentHistoryTrackingKit(\n    // ... other parameters\n    logger: MyLogger(),\n    logLevel: 2 // 0: off, 1: important, 2: detailed\n)\n```\n\n### Multiple Hooks with Execution Order\n\nObserver Hooks execute in **registration order**:\n\n```swift\n// These execute sequentially: Hook 1 → Hook 2 → Hook 3\nlet hook1 = await kit.registerObserver(entityName: \"Person\", operation: .insert) { _ in\n    print(\"Hook 1\")\n}\n\nlet hook2 = await kit.registerObserver(entityName: \"Person\", operation: .insert) { _ in\n    print(\"Hook 2\")\n}\n\nlet hook3 = await kit.registerObserver(entityName: \"Person\", operation: .insert) { _ in\n    print(\"Hook 3\")\n}\n\n// Remove only Hook 2\nawait kit.removeObserver(id: hook2)\n// Now only Hook 1 and Hook 3 execute\n```\n\nMerge Hooks support **pipeline insertion**:\n\n```swift\nlet hookA = await kit.registerMergeHook { _ in\n    print(\"Hook A\")\n    return .goOn\n}\n\n// Insert before hookA\nlet hookB = await kit.registerMergeHook(before: hookA) { _ in\n    print(\"Hook B\")\n    return .goOn\n}\n\n// Execution order: Hook B → Hook A\n```\n\n---\n\n## Requirements\n\n- iOS 13.0+ / macOS 10.15+ / macCatalyst 13.0+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+\n- Swift 6.0+\n- Xcode 16.0+\n\nRuntime validation in the current toolchain environment has been performed on iOS 15+.\nOlder declared deployment targets are compiler-checked but have not been runtime-validated here.\n\n---\n\n## Documentation\n\n- **[Hook Mechanism Guide](Docs/HookMechanism.md)** - Complete guide to Observer and Merge Hooks\n- **[Migration Guide](Docs/MigrationGuide.md)** - API, behavior, and platform changes from V1 to V2\n- **[Core Data Persistent History Tracking](https://fatbobman.com/en/posts/persistenthistorytracking/)** - Blog post on the fundamentals\n\n---\n\n## Testing\n\nTests are validated under parallel execution. The test infrastructure serializes `NSPersistentContainer` creation internally to avoid Core Data store-loading crashes while preserving parallel suite execution.\n\nCurrent runtime validation has been performed on iOS 15+.\nAlthough the package declares support for older OS versions, iOS 13 and iOS 14 have not been runtime-validated in the current Xcode environment.\n\n### iOS 13-14 Users\n\nIf you are running this library on iOS 13 or iOS 14:\n\n- These versions are declared as supported by the package, but have not yet been runtime-validated by the maintainer in the current toolchain environment.\n- If you encounter an issue, please include your device model, iOS version, and reproduction steps when opening an issue.\n- If the library runs correctly for you on iOS 13 or iOS 14, feedback is also welcome and helps improve confidence in older OS compatibility.\n\n### Recommended: Use the test script\n\n```bash\n# Run all tests in parallel (recommended)\n./test.sh\n```\n\nThe test script ensures:\n\n- ✅ Full-suite parallel execution\n- ✅ Core Data concurrency assertions enabled\n- ✅ Reliable results\n\n### Alternative: Manual testing (caution required)\n\nIf you run tests manually, prefer the same parallel settings:\n\n```bash\n# Run the full suite in parallel\nswift test --parallel\n\n# Or run an individual suite\nswift test --filter HookRegistryActorTests\n```\n\nTest suites include:\n\n- Unit tests for all actors and components\n- Integration tests with real Core Data stack\n- Concurrency stress tests\n- Memory leak detection\n- Hook system tests (Observer and Merge Hooks)\n\n---\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n### Development Setup\n\n```bash\ngit clone https://github.com/fatbobman/PersistentHistoryTrackingKit.git\ncd PersistentHistoryTrackingKit\nswift build\n./test.sh\n```\n\n---\n\n## License\n\nThis library is released under the MIT license. See [LICENSE](LICENSE) for details.\n\n---\n\n## Author\n\n**Fatbobman (肘子)**\n\n- Blog: [fatbobman.com](https://fatbobman.com)\n- Newsletter: [Fatbobman's Swift Weekly](https://weekly.fatbobman.com)\n- Twitter: [@fatbobman](https://twitter.com/fatbobman)\n\n---\n\n## Acknowledgments\n\nThanks to the Swift and Core Data communities for their valuable feedback and contributions.\n\nSpecial thanks to contributors who helped improve V2:\n- Community members who submitted PRs for undo manager handling and deduplication strategies\n- Early testers of the Swift 6 migration\n\n---\n\n## Sponsor\n\nIf you find this library helpful, consider supporting my work:\n\n\u003ca href=\"https://buymeacoffee.com/fatbobman\" target=\"_blank\"\u003e\n\u003cimg src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 60px !important;width: 217px !important;\" \u003e\n\u003c/a\u003e\n\n**[☕ Buy Me a Coffee](https://buymeacoffee.com/fatbobman)**\n\nYour support helps me continue maintaining and improving open-source Swift libraries. Thank you! 🙏\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffatbobman%2Fpersistenthistorytrackingkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffatbobman%2Fpersistenthistorytrackingkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffatbobman%2Fpersistenthistorytrackingkit/lists"}