[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.build\n/.swiftpm\n**/Package.resolved\n/Packages\n\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata/\n\n*.moved-aside\n*.xccheckout\n*.xcscmblueprint\n.claude/\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What is LLVS?\n\nLLVS (Low-Level Versioned Store) is a decentralized, versioned key-value storage framework — essentially Git for app data. It provides version-controlled data storage with branching, merging, and syncing across devices and processes (main app, extensions, watch apps). The data itself is opaque to the framework; apps store arbitrary `Data` blobs keyed by string identifiers.\n\n## Build & Test Commands\n\n```bash\nswift build                                    # Build all targets\nswift test                                     # Run all 170 tests\nswift test --filter LLVSTests.StoreSetupTests  # Run a single test class\nswift test --filter testStoreCreatesDirectories # Run a single test method by name\n```\n\nTests are in `Tests/LLVSTests/` and `Tests/LLVSModelTests/`, depending on `LLVS`, `LLVSSQLite`, and `LLVSModel`. There is no linter configured. Sample apps (in `Samples/`) are Xcode projects, not part of the SPM package.\n\n## Package Structure\n\nSPM targets with a layered dependency graph:\n\n- **LLVS** — Core framework, zero dependencies. All fundamental types and logic.\n- **LLVSModel** — High-level model layer with `@MergeableModel` macro, `StorableModel` protocol, and `MergeableArbiter`. Depends on LLVS.\n- **LLVSModelMacros** — Swift macro implementation for `@MergeableModel`. Depends on SwiftSyntax.\n- **LLVSSQLite** — SQLite storage backend. Depends on LLVS and SQLite3.\n- **LLVSCloudKit** — CloudKit sync exchange. Depends on LLVS.\n- **SQLite3** — System library wrapper for SQLite.\n\n## Architecture\n\n### Core Data Flow\n\n`Store` is the central class. It owns a `History` (in-memory DAG of all versions), a `Map` (index mapping versions to their values), and a `Zone` (pluggable storage backend). All writes go through `Store.makeVersion()`, which atomically records a new version with its value changes. All reads go through `Store.value(id:at:)`, which resolves what value exists for a key at a given version by walking the map.\n\n`StoreCoordinator` wraps `Store` with convenience: it tracks the \"current version\" for the app UI, simplifies save/fetch, and orchestrates exchange + merge cycles.\n\n### Version History (DAG)\n\nVersions form a directed acyclic graph. Each `Version` has 0-2 predecessors (0 for initial, 1 for linear, 2 for merge commits) and 0+ successors. \"Heads\" are versions with no successors — the branch tips. `History` provides traversal (topological sort via Kahn's algorithm), common ancestor finding, and head tracking. Access to `History` is serialized via `historyAccessQueue` — always use `store.queryHistory { history in ... }`.\n\n### Merging\n\nThree-way merge is the primary merge strategy: find the greatest common ancestor of two heads, diff each head against it, then pass the forks to a `MergeArbiter` to resolve conflicts. The `MergeArbiter` protocol has a single method: `changes(toResolve:in:) throws -> [Value.Change]`. Built-in arbiters: `MostRecentBranchFavoringArbiter` (favors branch with newer timestamp), `MostRecentChangeFavoringArbiter` (favors most recent individual change), and `MergeableArbiter` (delegates to `Mergeable` types for property-wise 3-way merge). Fast-forward is used when one version is an ancestor of the other.\n\n`@MergeableModel` macro generates `Mergeable` conformance for structs, producing per-property merge via overloaded `mergeProperty`/`salvageProperty` helpers. Properties conforming to `Mergeable` get deep recursive merge; plain `Equatable` properties use simple equality checks. `Optional<Wrapped>` where `Wrapped: Mergeable` also supports smart merge.\n\n`Value.Fork` describes per-value conflict states: `.inserted`, `.updated`, `.removed` (non-conflicting, single branch), `.twiceInserted`, `.twiceUpdated`, `.removedAndUpdated` (conflicting, require arbiter resolution).\n\n### Storage Abstraction\n\n`Storage` protocol creates `Zone` instances. `Zone` is the raw read/write interface (`store(_:for:)` / `data(for:)`). Two implementations:\n- `FileZone` — hierarchical files on disk under the store's root directory. Uses 2-char prefix subdirectories for filesystem efficiency. Multi-process safe.\n- `SQLiteZone` — SQLite-backed storage via `LLVSSQLite`. Not thread-safe by design (caller manages concurrency).\n\n### Sync (Exchange)\n\n`Exchange` protocol handles sending/receiving versions between stores. All exchange methods use async/await. The default `retrieve`/`send` implementations orchestrate the full sync flow: discover remote version IDs → find missing ones → fetch/push in batches (5MB chunks via `DynamicTaskBatcher`). Implementations:\n- `CloudKitExchange` — syncs via CloudKit (private, public, or shared databases).\n- `FileSystemExchange` — syncs via a shared filesystem directory (useful for testing).\n- `MemoryExchange` — actor-based in-memory exchange.\n- `MultipeerExchange` — peer-to-peer exchange using `PeerTransport` protocol.\n\n### Map (Value Index)\n\n`Map` is a hierarchical tree that tracks which values exist at each version. Nodes are keyed by 2-character prefixes of value identifiers, forming a trie-like structure. This allows efficient \"what values changed in this version?\" queries without scanning all values.\n\n### Key Value Types\n\n- `Value` — has an `ID` (string key) and `Data` payload, plus an optional `Reference` (version + key) for locating stored data.\n- `Value.Change` — enum: `.insert`, `.update`, `.remove`, `.preserve`, `.preserveRemoval`. These are what get stored per-version.\n- `Version.ID` — wrapper around a UUID string.\n- `Branch` — wrapper around a raw string, stored in version metadata.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "### Low-Level Versioned Store (LLVS) – Contributor License Agreement (v1)\n\nThank you for your interest in the Low-Level Versioned Store [LLVS] (the \"Project\"). In order to clarify the intellectual property license granted with Contributions from any person or entity, the Project must have a Contributor License Agreement (\"CLA\") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of the Project and its users; it does not change your rights to use your own Contributions for any other purpose. If you have not already done so, please complete and sign, then submit this Agreement by filling this electronic form and pressing the Submit button at the end of this electronic form.  Please read this document carefully before signing and keep a copy for your records.\n\nYou accept and agree to the following terms and conditions for Your present and future Contributions submitted to the Project. In return, the Project shall not use Your Contributions in a way that is contrary to the public benefit. Except for the license granted herein to the Project and recipients of software distributed by the Project, You reserve all right, title, and interest in and to Your Contributions.\n\n1. Definitions.\n\n\"You\" (or \"Your\") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with the Project. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"Contribution\" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the Project for inclusion in, or documentation of, any of the products owned or managed by the Project (the \"Work\"). For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as \"Not a Contribution.\"\n\n2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to the Project and to recipients of software distributed by the Project a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, distribute, and otherwise exploit in any manner Your Contributions and such derivative works.\n\n3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to the Project and to recipients of software distributed by the Project a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, exploit in any manner, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.\n\n4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to the Project, or that your employer has executed a separate Corporate CLA with the Project.\n\n5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).  You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.\n\n6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.\n\n7. Should You wish to submit work that is not Your original creation, You may submit it to the Project separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as \"Submitted on behalf of a third-party: [named here]\".\n\n8. You agree to notify the Project of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.\n"
  },
  {
    "path": "LICENCE.txt",
    "content": " Copyright (c) 2019 Drew McCormack\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the \"Software\"), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n\n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version: 6.1\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\nimport CompilerPluginSupport\n\nlet package = Package(\n    name: \"LLVS\",\n    platforms: [\n        .macOS(.v15), .iOS(.v18), .watchOS(.v11)\n    ],\n    products: [\n        .library(\n            name: \"SQLite3\",\n            targets: [\"SQLite3\"]),\n        .library(\n            name: \"LLVS\",\n            targets: [\"LLVS\"]),\n        .library(\n            name: \"LLVSCloudKit\",\n            targets: [\"LLVSCloudKit\"]),\n        .library(\n            name: \"LLVSSQLite\",\n            targets: [\"LLVSSQLite\"]),\n        .library(\n            name: \"LLVSPCloud\",\n            targets: [\"LLVSPCloud\"]),\n        .library(\n            name: \"LLVSBox\",\n            targets: [\"LLVSBox\"]),\n        .library(\n            name: \"LLVSModel\",\n            targets: [\"LLVSModel\"]),\n        .library(\n            name: \"LLVSWebDAV\",\n            targets: [\"LLVSWebDAV\"]),\n        .library(\n            name: \"LLVSGoogleDrive\",\n            targets: [\"LLVSGoogleDrive\"]),\n        .library(\n            name: \"LLVSOneDrive\",\n            targets: [\"LLVSOneDrive\"]),\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/weichsel/ZIPFoundation.git\", .upToNextMajor(from: \"0.9.0\")),\n        .package(url: \"https://github.com/pCloud/pcloud-sdk-swift.git\", from: \"3.0.0\"),\n        .package(url: \"https://github.com/box/box-ios-sdk.git\", from: \"10.0.0\"),\n        .package(url: \"https://github.com/swiftlang/swift-syntax.git\", from: \"600.0.0\"),\n    ],\n    targets: [\n        .systemLibrary(\n            name: \"SQLite3\"\n        ),\n        .target(\n            name: \"LLVS\",\n            dependencies: [\n                .product(name: \"ZIPFoundation\", package: \"ZIPFoundation\"),\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .testTarget(\n            name: \"LLVSTests\",\n            dependencies: [\"LLVS\", \"LLVSSQLite\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSCloudKit\",\n            dependencies: [\"LLVS\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSSQLite\",\n            dependencies: [\"LLVS\", \"SQLite3\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSPCloud\",\n            dependencies: [\n                \"LLVS\",\n                .product(name: \"PCloudSDKSwift\", package: \"pcloud-sdk-swift\")\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSBox\",\n            dependencies: [\n                \"LLVS\",\n                .product(name: \"BoxSDK\", package: \"box-ios-sdk\")\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .macro(\n            name: \"LLVSModelMacros\",\n            dependencies: [\n                .product(name: \"SwiftSyntaxMacros\", package: \"swift-syntax\"),\n                .product(name: \"SwiftCompilerPlugin\", package: \"swift-syntax\"),\n            ]),\n        .target(\n            name: \"LLVSModel\",\n            dependencies: [\n                \"LLVS\",\n                \"LLVSModelMacros\",\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSWebDAV\",\n            dependencies: [\"LLVS\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSGoogleDrive\",\n            dependencies: [\"LLVS\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .target(\n            name: \"LLVSOneDrive\",\n            dependencies: [\"LLVS\"],\n            swiftSettings: [.swiftLanguageMode(.v5)]),\n        .testTarget(\n            name: \"LLVSModelTests\",\n            dependencies: [\n                \"LLVSModel\",\n                \"LLVS\",\n                \"LLVSSQLite\",\n            ],\n            swiftSettings: [.swiftLanguageMode(.v5)])\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmentalfaculty%2FLLVS%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/mentalfaculty/LLVS)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmentalfaculty%2FLLVS%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/mentalfaculty/LLVS)\n\n# Low-Level Versioned Store (LLVS)\n\n_Author: Drew McCormack ([@drewmccormack](https://github.com/drewmccormack))_\n\nEver wish it was as easy to move your app's data around as it is to push and pull your source code with Git?\n\nLLVS brings the same model to app data. Every save creates a version. Versions branch, merge, and sync between devices -- just like commits in a Git repository. Your app gets full version history, conflict resolution, and multi-device sync without writing any networking or diffing code.\n\n### The problem\n\nA user edits a note on their phone during a flight. Meanwhile, a share extension updates the same note on their iPad. Later, their Watch app writes a quick addition. When the phone comes back online, three copies of the data have diverged independently.\n\nKeeping these in sync is the kind of problem that consumes months of development time. You end up writing custom conflict detection, manual diffing, retry logic, and timestamp heuristics -- and it's still fragile.\n\nLLVS handles this the way Git handles divergent branches: it tracks the full ancestry of every change, finds the common ancestor when versions diverge, and merges them back together through a conflict resolver you control. The framework does the hard part; you just decide what \"resolve this conflict\" means for your data.\n\n### What you get\n\n- **Version history** -- Every save is a version. Branch, merge, diff, or revert to any point in time.\n- **Three-way merge** -- When versions diverge, LLVS finds their common ancestor and diffs both sides. You provide a `MergeArbiter` to resolve conflicts however you like, or use a built-in one.\n- **Sync without networking code** -- Push and pull versions between stores via CloudKit, a shared filesystem, or your own custom exchange. Attach multiple exchanges to the same store.\n- **Multi-process safe** -- Share a store between your main app, extensions, and widgets using an app group container. LLVS handles concurrent access.\n- **Pluggable storage** -- File-based storage by default, SQLite via `LLVSSQLite`, or bring your own backend.\n- **Encryption-friendly** -- LLVS stores opaque `Data` blobs. Encrypt them however you want; the framework never inspects your data.\n\n\n## Quick Start\n\nThis walks through a minimal app that syncs a shared message via CloudKit. Full code is in _Samples/TheMessage_.\n\n### Set up a StoreCoordinator\n\n`StoreCoordinator` is the simplest entry point. It wraps a `Store`, tracks the current version, and orchestrates sync and merging.\n\n```swift\nlazy var storeCoordinator: StoreCoordinator = {\n    let coordinator = try! StoreCoordinator()\n    let container = CKContainer(identifier: \"iCloud.com.mycompany.themessage\")\n    let exchange = CloudKitExchange(\n        with: coordinator.store,\n        storeIdentifier: \"MainStore\",\n        cloudDatabaseDescription: .publicDatabase(container)\n    )\n    coordinator.exchange = exchange\n    return coordinator\n}()\n```\n\n### Save, fetch, sync\n\n```swift\nlet messageId = Value.ID(\"MESSAGE\")\n\nfunc post(message: String) {\n    let value = Value(id: messageId, data: message.data(using: .utf8)!)\n    try! storeCoordinator.save(updating: [value])\n    sync()\n}\n\nfunc fetchMessage() -> String? {\n    guard let value = try? storeCoordinator.value(id: messageId) else { return nil }\n    return String(data: value.data, encoding: .utf8)\n}\n\nfunc sync() {\n    storeCoordinator.exchange { _ in\n        self.storeCoordinator.merge()\n    }\n}\n```\n\n`exchange` sends and receives versions with the cloud. `merge` reconciles any concurrent changes. That's the entire sync implementation.\n\nThis example is deliberately minimal -- the real power of LLVS shows up when data diverges across devices, which is covered below.\n\n\n## Installation\n\n### Swift Package Manager\n\nAdd LLVS as a dependency in your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/mentalfaculty/LLVS.git\", from: \"0.3.0\")\n]\n```\n\nThen add the libraries you need: `LLVS` for the core framework, `LLVSSQLite` for SQLite-backed storage, and `LLVSCloudKit` for CloudKit sync.\n\n### Xcode\n\nChoose _File > Add Package Dependencies..._, enter the LLVS repository URL, and select the libraries your target needs.\n\n### Platforms\n\nmacOS 10.15+, iOS 13+, watchOS 6+.\n\n\n## Working with `Store`\n\n`StoreCoordinator` is convenient for common cases, but `Store` gives you direct access to the version graph -- branching, merging, diffing, and time travel.\n\n### Creating a Store\n\n```swift\nlet rootDir = FileManager.default\n    .containerURL(forSecurityApplicationGroupIdentifier: \"group.com.mycompany.myapp\")!\n    .appendingPathComponent(\"MyStore\")\nlet store = try Store(rootDirectoryURL: rootDir)\n```\n\nUsing an app group container lets your main app, extensions, and widgets share the same store. For SQLite-backed storage:\n\n```swift\nlet store = try Store(rootDirectoryURL: rootDir, storage: SQLiteStorage())\n```\n\n### Versions and values\n\nEvery write creates a new version:\n\n```swift\nlet value = Value(idString: \"ABCDEF\", data: \"Hello\".data(using: .utf8)!)\nlet firstVersion = try store.makeVersion(basedOnPredecessor: nil, inserting: [value])\n```\n\nPassing `nil` for the predecessor creates the initial version -- like Git's first commit. Subsequent changes build on a predecessor:\n\n```swift\nlet updated = Value(idString: \"ABCDEF\", data: \"World\".data(using: .utf8)!)\nlet secondVersion = try store.makeVersion(\n    basedOnPredecessor: firstVersion.id,\n    updating: [updated]\n)\n```\n\nInserts, updates, and removes can be combined in a single call:\n\n```swift\nlet thirdVersion = try store.makeVersion(\n    basedOnPredecessor: secondVersion.id,\n    inserting: [newValue],\n    updating: [changedValue],\n    removing: [obsoleteValueId]\n)\n```\n\nVersions are store-wide: once a value is added, it persists in all subsequent versions until explicitly updated or removed. You can retrieve any value at any version:\n\n```swift\nlet value = try store.value(idString: \"ABCDEF\", at: secondVersion.id)!\n```\n\n### Branching and heads\n\nWhen concurrent changes happen -- edits on two devices between syncs, or writes from both your app and its share extension -- the version history naturally diverges into branches. This isn't an error; it's the normal state of decentralized data. The branches get reconciled through merging.\n\nEach `Version` can have up to two predecessors (one for linear history, two for merge commits) and any number of successors. A _head_ is a version with no successors -- the tip of a branch. When multiple heads exist, they generally need to be merged.\n\n```swift\nstore.queryHistory { history in\n    let heads = history.headIdentifiers\n    // ...\n}\n\n// Or get the most recent head directly:\nlet latest: Version? = store.mostRecentHead\n```\n\n### Merging\n\nThis is where it gets interesting. When two versions have diverged, LLVS performs a three-way merge: it finds the greatest common ancestor, diffs each branch against it, and hands the results to a `MergeArbiter` that you provide. The arbiter decides how to resolve every conflict.\n\n```swift\nlet arbiter = MostRecentChangeFavoringArbiter()\nlet merged = try store.merge(version: headA, with: headB, resolvingWith: arbiter)\n```\n\nIf one version is an ancestor of the other, LLVS fast-forwards without creating a new version -- just like Git.\n\nLLVS ships with two built-in arbiters:\n\n- `MostRecentChangeFavoringArbiter` -- resolves each conflict individually by keeping whichever change is newer.\n- `MostRecentBranchFavoringArbiter` -- resolves all conflicts by favoring whichever branch has the newer timestamp.\n\nFor full control, implement the `MergeArbiter` protocol:\n\n```swift\npublic protocol MergeArbiter {\n    func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change]\n}\n```\n\nThe `Merge` object gives you a dictionary of `Value.Fork` entries describing per-value conflict states: `.inserted`, `.updated`, `.removed` (non-conflicting, single branch), `.twiceInserted`, `.twiceUpdated`, `.removedAndUpdated` (conflicting, both branches changed). Your arbiter returns `Value.Change` entries that resolve all the conflicting forks. This is where you encode your app's domain logic -- maybe the longer text wins, maybe you concatenate both, maybe you prompt the user.\n\n### Sync (Exchange)\n\nAn `Exchange` sends and receives versions between stores -- the equivalent of `git push` and `git pull`.\n\n**CloudKit** (via the `LLVSCloudKit` library):\n\n```swift\nlet exchange = CloudKitExchange(\n    with: store,\n    storeIdentifier: \"MyStore\",\n    cloudDatabaseDescription: .privateDatabaseWithCustomZone(\n        CKContainer.default(), zoneIdentifier: \"MyZone\"\n    )\n)\n```\n\n**File system** (useful for testing and inter-process sync):\n\n```swift\nlet exchange = FileSystemExchange(\n    rootDirectoryURL: sharedDirectoryURL, store: store\n)\n```\n\nRetrieving and sending are both asynchronous:\n\n```swift\nexchange.retrieve { result in /* handle result */ }\nexchange.send { result in /* handle result */ }\n```\n\nYou can attach multiple exchanges to a single store, syncing via different routes simultaneously -- CloudKit for cross-device, a shared directory for inter-process. You can also implement custom exchanges by conforming to the `Exchange` protocol.\n\n\n## Structuring Your Data\n\nLLVS stores opaque `Data` blobs keyed by string identifiers. How you map your model onto values is up to you, but the granularity matters:\n\n| Approach | Merging | Performance | Disk use |\n|---|---|---|---|\n| **One property per Value** | Best (per-property conflict resolution) | Slow (many small reads) | Many small files |\n| **One entity per Value** | Good (per-entity conflict resolution) | Moderate | Moderate |\n| **Entire model in one Value** | Poor (must merge everything manually) | Fast (single read) | Large per-version files |\n\n**One entity per Value** is a good default. It gives you per-entity conflict resolution while keeping read performance reasonable. Use `Codable`, JSON, flatbuffers, or whatever serialization you prefer -- LLVS never inspects the bytes.\n\n\n## Snapshots\n\nWhen a new device joins your sync group, it normally replays every version from the beginning -- downloading each change and rebuilding the store's history. For stores with thousands of versions, this can be slow.\n\n**Cloud snapshots** solve this by periodically uploading a chunked dump of the entire store. A new device downloads the snapshot, restores it locally, and then uses normal incremental sync to catch up with any versions added since the snapshot. Existing devices are completely unaffected.\n\n### Bootstrapping a new device\n\n```swift\nlet coordinator = try StoreCoordinator(\n    withStoreDirectoryAt: storeURL,\n    cacheDirectoryAt: cacheURL,\n    snapshotPolicy: .auto\n)\ncoordinator.exchange = myExchange\n\n// On first launch, try to restore from a snapshot before syncing\ncoordinator.bootstrapFromSnapshot { error in\n    coordinator.exchange { _ in\n        coordinator.merge()\n    }\n}\n```\n\n`bootstrapFromSnapshot()` checks whether the exchange supports snapshots, whether one exists, and whether the local store is empty. If all conditions are met, it downloads and restores the snapshot. If not, it completes immediately -- the app falls back to a full sync with no extra code.\n\n### Automatic snapshot uploads\n\nWith `SnapshotPolicy.auto`, the coordinator uploads a new snapshot after each exchange when enough time has passed (`minimumInterval`, default 7 days) and enough new versions have accumulated (`minimumNewVersions`, default 20). Use `.disabled` (the default) to opt out.\n\n### Custom storage and exchange support\n\nSnapshot support requires both the storage backend and the exchange to opt in:\n\n- **Storage**: Conform to `SnapshotCapable` (both `FileStorage` and `SQLiteStorage` already do).\n- **Exchange**: Conform to `SnapshotExchange` (`FileSystemExchange` already does).\n\nIf either side doesn't conform, snapshot operations are silently skipped.\n\n\n## Architecture\n\nThis section covers the internal design for contributors and anyone who wants to understand what's happening under the hood.\n\n### Package structure\n\nLLVS is split into four SPM targets:\n\n| Target | Purpose | Dependencies |\n|---|---|---|\n| **LLVS** | Core framework: `Store`, `History`, `Map`, `Zone`, `Exchange`, `Value`, `Version` | None |\n| **LLVSSQLite** | SQLite storage backend | LLVS, SQLite3 |\n| **LLVSCloudKit** | CloudKit sync exchange | LLVS |\n| **SQLite3** | System library wrapper | System SQLite |\n\n### Core data flow\n\n`Store` is the central class. It owns three components:\n\n- **History** -- An in-memory directed acyclic graph (DAG) of all versions. Provides topological traversal (Kahn's algorithm), common ancestor finding, and head tracking. Access is serialized via `historyAccessQueue`.\n- **Map** -- A hierarchical trie-like index that tracks which values exist at each version. Nodes are keyed by 2-character prefixes of value identifiers, making \"what values exist at this version?\" queries efficient without scanning everything.\n- **Zone** -- A pluggable storage backend for reading and writing raw data.\n\nAll writes go through `Store.makeVersion()`, which atomically records a new version with its value changes. All reads go through `Store.value(id:at:)`, which walks the map to find where a value's data is physically stored.\n\n### Storage abstraction\n\nThe `Storage` protocol creates `Zone` instances. `Zone` is the raw read/write interface with two methods: `store(_:for:)` and `data(for:)`.\n\n- **FileZone** -- Files on disk, using 2-character prefix subdirectories for filesystem efficiency. Multi-process safe.\n- **SQLiteZone** -- SQLite-backed. Not thread-safe by design (the caller manages concurrency). Available via `LLVSSQLite`.\n\n### Map internals\n\nThe Map is a two-level trie. The root node for each version points to subnodes keyed by the first two characters of value identifiers. Each subnode maps value IDs to `Value.Reference` (which records the version where the data is physically stored).\n\nSubnodes are shared across versions -- if a version doesn't modify any values in a particular bucket, it reuses the parent's subnode. This makes versioning space-efficient.\n\n\n## Samples\n\nThe _Samples_ directory includes three example projects:\n\n- **TheMessage** -- A minimal app that syncs a single shared message via CloudKit. Good for understanding the basics.\n- **LoCo** -- A contact book app using UIKit.\n- **LoCo-SwiftUI** -- The same contact book app built with SwiftUI.\n\n\n## Learning More\n\nThere are useful posts at the [LLVS Blog](https://mentalfaculty.github.io/LLVS/).\n"
  },
  {
    "path": "Samples/LoCo/LoCo/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/Contact.swift",
    "content": "//\n//  Contact.swift\n//  LoCo\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport Foundation\nimport LLVS\nimport LLVSModel\n\n@MergeableModel\nstruct Contact: StorableModel, Equatable, Identifiable, Codable {\n    static let modelTypeIdentifier = \"Contact\"\n    var id: UUID = .init()\n    var firstName: String = \"\"\n    var lastName: String = \"\"\n    var streetAddress: String = \"\"\n    var postCode: String = \"\"\n    var city: String = \"\"\n    var country: String = \"\"\n    var avatarJPEGData: Data?\n\n    var fullName: String {\n        switch (firstName.isEmpty, lastName.isEmpty) {\n        case (true, true):\n            return \"\"\n        case (true, false):\n            return lastName\n        case (false, true):\n            return firstName\n        case (false, false):\n            return \"\\(firstName) \\(lastName)\"\n        }\n    }\n\n    var fullNameOrPlaceholder: String {\n        fullName.isEmpty ? \"New Contact\" : fullName\n    }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/ContactStore.swift",
    "content": "//\n//  ContactStore.swift\n//  LoCo\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport Foundation\nimport LLVS\nimport LLVSModel\nimport LLVSCloudKit\nimport CloudKit\n\n@MainActor @Observable\nclass ContactStore {\n    var contacts: [Contact] = []\n\n    private let storeCoordinator: StoreCoordinator\n    @ObservationIgnored nonisolated(unsafe) private var versionTask: Task<Void, Never>?\n    @ObservationIgnored nonisolated(unsafe) private var pollingTask: Task<Void, Never>?\n\n    init() {\n        LLVS.log.level = .verbose\n\n        let coordinator = try! StoreCoordinator()\n\n        let arbiter = MergeableArbiter()\n        arbiter.register(Contact.self)\n        coordinator.mergeArbiter = arbiter\n\n        let container = CKContainer(identifier: \"iCloud.com.mentalfaculty.loco\")\n        let exchange = CloudKitExchange(with: coordinator.store, storeIdentifier: \"MainStore\", cloudDatabaseDescription: .privateDatabaseWithCustomZone(container, zoneIdentifier: \"LoCo\"))\n        coordinator.exchange = exchange\n\n        self.storeCoordinator = coordinator\n\n        contacts = fetchContacts()\n\n        versionTask = Task { [weak self] in\n            guard let self else { return }\n            for await _ in coordinator.currentVersionUpdates {\n                self.contacts = self.fetchContacts()\n            }\n        }\n\n        startPolling()\n    }\n\n    private func fetchContacts() -> [Contact] {\n        (try? storeCoordinator.fetchAllModels(Contact.self)) ?? []\n    }\n\n    func addContact() -> Contact {\n        let contact = Contact()\n        try! storeCoordinator.save(contact, instanceIdentifier: contact.id.uuidString)\n        sync()\n        return contact\n    }\n\n    func updateContact(_ contact: Contact) {\n        try! storeCoordinator.save(contact, instanceIdentifier: contact.id.uuidString)\n        sync()\n    }\n\n    func deleteContact(_ contact: Contact) {\n        try! storeCoordinator.removeModel(Contact.self, instanceIdentifier: contact.id.uuidString)\n        sync()\n    }\n\n    func sync() {\n        Task {\n            try? await storeCoordinator.exchange()\n            storeCoordinator.merge()\n        }\n    }\n\n    private func startPolling() {\n        pollingTask = Task {\n            while !Task.isCancelled {\n                try? await Task.sleep(for: .seconds(15))\n                try? await storeCoordinator.exchange()\n                storeCoordinator.merge()\n            }\n        }\n    }\n\n    deinit {\n        versionTask?.cancel()\n        pollingTask?.cancel()\n    }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/ContactView.swift",
    "content": "//\n//  ContactView.swift\n//  LoCo\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftUI\n\nstruct ContactView: View {\n    @Environment(ContactStore.self) var store\n    let contactId: UUID\n\n    private var contact: Contact? {\n        store.contacts.first(where: { $0.id == contactId })\n    }\n\n    var body: some View {\n        if var contact {\n            Form {\n                Section(\"Name\") {\n                    TextField(\"First Name\", text: binding(for: \\.firstName, on: &contact))\n                    TextField(\"Last Name\", text: binding(for: \\.lastName, on: &contact))\n                }\n                Section(\"Address\") {\n                    TextField(\"Street\", text: binding(for: \\.streetAddress, on: &contact))\n                    TextField(\"Post Code\", text: binding(for: \\.postCode, on: &contact))\n                    TextField(\"City\", text: binding(for: \\.city, on: &contact))\n                    TextField(\"Country\", text: binding(for: \\.country, on: &contact))\n                }\n            }\n            .navigationTitle(contact.fullNameOrPlaceholder)\n        } else {\n            ContentUnavailableView(\"Contact Not Found\", systemImage: \"person.slash\")\n        }\n    }\n\n    private func binding(for keyPath: WritableKeyPath<Contact, String>, on contact: inout Contact) -> Binding<String> {\n        Binding(\n            get: { self.contact?[keyPath: keyPath] ?? \"\" },\n            set: { newValue in\n                if var updated = self.contact {\n                    updated[keyPath: keyPath] = newValue\n                    store.updateContact(updated)\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/ContactsView.swift",
    "content": "//\n//  ContactsView.swift\n//  LoCo\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftUI\n\nstruct ContactsView: View {\n    @Environment(ContactStore.self) var store\n\n    var body: some View {\n        NavigationStack {\n            List {\n                ForEach(store.contacts.sorted(by: { $0.fullNameOrPlaceholder < $1.fullNameOrPlaceholder })) { contact in\n                    NavigationLink(value: contact.id) {\n                        VStack(alignment: .leading) {\n                            Text(contact.fullNameOrPlaceholder)\n                                .font(.headline)\n                            if !contact.city.isEmpty {\n                                Text(contact.city)\n                                    .font(.subheadline)\n                                    .foregroundStyle(.secondary)\n                            }\n                        }\n                    }\n                }\n                .onDelete(perform: deleteContacts)\n            }\n            .navigationTitle(\"Contacts\")\n            .navigationDestination(for: UUID.self) { contactId in\n                ContactView(contactId: contactId)\n            }\n            .toolbar {\n                ToolbarItem(placement: .primaryAction) {\n                    Button {\n                        _ = store.addContact()\n                    } label: {\n                        Image(systemName: \"plus\")\n                    }\n                }\n            }\n        }\n    }\n\n    private func deleteContacts(at offsets: IndexSet) {\n        let sorted = store.contacts.sorted(by: { $0.fullNameOrPlaceholder < $1.fullNameOrPlaceholder })\n        for index in offsets {\n            store.deleteContact(sorted[index])\n        }\n    }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/LoCo.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.icloud-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.com.mentalfaculty.loco</string>\n\t</array>\n\t<key>com.apple.developer.icloud-services</key>\n\t<array>\n\t\t<string>CloudKit</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Samples/LoCo/LoCo/LoCoApp.swift",
    "content": "//\n//  LoCoApp.swift\n//  LoCo\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftUI\n\n@main\nstruct LoCoApp: App {\n    @State private var store = ContactStore()\n\n    var body: some Scene {\n        WindowGroup {\n            ContactsView()\n                .environment(store)\n        }\n    }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Samples/LoCo/LoCo.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\tAA0001012D97B1E200000001 /* LoCoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002022D97B1E200000001 /* LoCoApp.swift */; };\n\t\tAA0001022D97B1E200000001 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002032D97B1E200000001 /* Contact.swift */; };\n\t\tAA0001042D97B1E200000001 /* ContactStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002052D97B1E200000001 /* ContactStore.swift */; };\n\t\tAA0001052D97B1E200000001 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002062D97B1E200000001 /* ContactsView.swift */; };\n\t\tAA0001062D97B1E200000001 /* ContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072D97B1E200000001 /* ContactView.swift */; };\n\t\tAA0001072D97B1E200000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA0002082D97B1E200000001 /* Assets.xcassets */; };\n\t\tAA0001082D97B1E200000001 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA0002092D97B1E200000001 /* Preview Assets.xcassets */; };\n\t\tAA0001092D97B1E200000001 /* LLVS in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A012D97B1E200000001 /* LLVS */; };\n\t\tAA00010A2D97B1E200000001 /* LLVSCloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A022D97B1E200000001 /* LLVSCloudKit */; };\n\t\tAA00010B2D97B1E200000001 /* LLVSModel in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A032D97B1E200000001 /* LLVSModel */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\tAA0002012D97B1E200000001 /* LoCo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LoCo.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tAA0002022D97B1E200000001 /* LoCoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoCoApp.swift; sourceTree = \"<group>\"; };\n\t\tAA0002032D97B1E200000001 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = \"<group>\"; };\n\t\tAA0002052D97B1E200000001 /* ContactStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactStore.swift; sourceTree = \"<group>\"; };\n\t\tAA0002062D97B1E200000001 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = \"<group>\"; };\n\t\tAA0002072D97B1E200000001 /* ContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactView.swift; sourceTree = \"<group>\"; };\n\t\tAA0002082D97B1E200000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\tAA0002092D97B1E200000001 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\tAA00020A2D97B1E200000001 /* LoCo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoCo.entitlements; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\tAA0004012D97B1E200000001 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tAA0001092D97B1E200000001 /* LLVS in Frameworks */,\n\t\t\t\tAA00010A2D97B1E200000001 /* LLVSCloudKit in Frameworks */,\n\t\t\t\tAA00010B2D97B1E200000001 /* LLVSModel in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\tAA0003012D97B1E200000001 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAA0003032D97B1E200000001 /* LoCo */,\n\t\t\t\tAA0003022D97B1E200000001 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tAA0003022D97B1E200000001 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAA0002012D97B1E200000001 /* LoCo.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tAA0003032D97B1E200000001 /* LoCo */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAA00020A2D97B1E200000001 /* LoCo.entitlements */,\n\t\t\t\tAA0002022D97B1E200000001 /* LoCoApp.swift */,\n\t\t\t\tAA0002032D97B1E200000001 /* Contact.swift */,\n\t\t\t\tAA0002052D97B1E200000001 /* ContactStore.swift */,\n\t\t\t\tAA0002062D97B1E200000001 /* ContactsView.swift */,\n\t\t\t\tAA0002072D97B1E200000001 /* ContactView.swift */,\n\t\t\t\tAA0002082D97B1E200000001 /* Assets.xcassets */,\n\t\t\t\tAA0003042D97B1E200000001 /* Preview Content */,\n\t\t\t);\n\t\t\tpath = LoCo;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tAA0003042D97B1E200000001 /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAA0002092D97B1E200000001 /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\tAA0005012D97B1E200000001 /* LoCo */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = AA0008022D97B1E200000001 /* Build configuration list for PBXNativeTarget \"LoCo\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tAA0004032D97B1E200000001 /* Sources */,\n\t\t\t\tAA0004012D97B1E200000001 /* Frameworks */,\n\t\t\t\tAA0004022D97B1E200000001 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = LoCo;\n\t\t\tpackageProductDependencies = (\n\t\t\t\tAA000A012D97B1E200000001 /* LLVS */,\n\t\t\t\tAA000A022D97B1E200000001 /* LLVSCloudKit */,\n\t\t\t\tAA000A032D97B1E200000001 /* LLVSModel */,\n\t\t\t);\n\t\t\tproductName = LoCo;\n\t\t\tproductReference = AA0002012D97B1E200000001 /* LoCo.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\tAA0006012D97B1E200000001 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 1600;\n\t\t\t\tLastUpgradeCheck = 2630;\n\t\t\t\tORGANIZATIONNAME = \"Momenta B.V.\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\tAA0005012D97B1E200000001 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 16.0;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = AA0008012D97B1E200000001 /* Build configuration list for PBXProject \"LoCo\" */;\n\t\t\tcompatibilityVersion = \"Xcode 14.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = AA0003012D97B1E200000001;\n\t\t\tpackageReferences = (\n\t\t\t\tAA0009012D97B1E200000001 /* XCLocalSwiftPackageReference \"../..\" */,\n\t\t\t);\n\t\t\tproductRefGroup = AA0003022D97B1E200000001 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\tAA0005012D97B1E200000001 /* LoCo */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\tAA0004022D97B1E200000001 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tAA0001082D97B1E200000001 /* Preview Assets.xcassets in Resources */,\n\t\t\t\tAA0001072D97B1E200000001 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\tAA0004032D97B1E200000001 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tAA0001012D97B1E200000001 /* LoCoApp.swift in Sources */,\n\t\t\t\tAA0001022D97B1E200000001 /* Contact.swift in Sources */,\n\t\t\t\tAA0001042D97B1E200000001 /* ContactStore.swift in Sources */,\n\t\t\t\tAA0001052D97B1E200000001 /* ContactsView.swift in Sources */,\n\t\t\t\tAA0001062D97B1E200000001 /* ContactView.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\tAA0007012D97B1E200000001 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"$(inherited) DEBUG\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tAA0007022D97B1E200000001 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tAA0007032D97B1E200000001 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = LoCo/LoCo.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"LoCo/Preview Content\\\"\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.loco;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tAA0007042D97B1E200000001 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = LoCo/LoCo.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"LoCo/Preview Content\\\"\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.loco;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\tAA0008012D97B1E200000001 /* Build configuration list for PBXProject \"LoCo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tAA0007012D97B1E200000001 /* Debug */,\n\t\t\t\tAA0007022D97B1E200000001 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tAA0008022D97B1E200000001 /* Build configuration list for PBXNativeTarget \"LoCo\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tAA0007032D97B1E200000001 /* Debug */,\n\t\t\t\tAA0007042D97B1E200000001 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCLocalSwiftPackageReference section */\n\t\tAA0009012D97B1E200000001 /* XCLocalSwiftPackageReference \"../..\" */ = {\n\t\t\tisa = XCLocalSwiftPackageReference;\n\t\t\trelativePath = ../..;\n\t\t};\n/* End XCLocalSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\tAA000A012D97B1E200000001 /* LLVS */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference \"../..\" */;\n\t\t\tproductName = LLVS;\n\t\t};\n\t\tAA000A022D97B1E200000001 /* LLVSCloudKit */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference \"../..\" */;\n\t\t\tproductName = LLVSCloudKit;\n\t\t};\n\t\tAA000A032D97B1E200000001 /* LLVSModel */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference \"../..\" */;\n\t\t\tproductName = LLVSModel;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = AA0006012D97B1E200000001 /* Project object */;\n}\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"60x60\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"size\" : \"60x60\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"20x20\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"29x29\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"40x40\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"76x76\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"76x76\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ipad\",\n      \"size\" : \"83.5x83.5\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"ios-marketing\",\n      \"size\" : \"1024x1024\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Samples/TheMessage/TheMessage/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Samples/TheMessage/TheMessage/ContentView.swift",
    "content": "import SwiftUI\n\nstruct ContentView: View {\n    @Environment(MessageStore.self) var store\n    @State private var draft: String = \"\"\n\n    var body: some View {\n        VStack(spacing: 20) {\n            Text(store.message)\n                .font(.title2)\n                .multilineTextAlignment(.center)\n                .padding()\n\n            HStack {\n                TextField(\"New message\", text: $draft)\n                    .textFieldStyle(.roundedBorder)\n                Button(\"Post\") {\n                    guard !draft.isEmpty else { return }\n                    store.post(message: draft)\n                    draft = \"\"\n                }\n                .buttonStyle(.borderedProminent)\n            }\n            .padding(.horizontal)\n        }\n    }\n}\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage/MessageStore.swift",
    "content": "import Foundation\nimport LLVS\nimport LLVSCloudKit\nimport CloudKit\n\n@MainActor @Observable\nclass MessageStore {\n    var message: String = \"Let there be light!\"\n\n    private let storeCoordinator: StoreCoordinator\n    private let messageId = Value.ID(\"MESSAGE\")\n    @ObservationIgnored nonisolated(unsafe) private var versionTask: Task<Void, Never>?\n    @ObservationIgnored nonisolated(unsafe) private var pollingTask: Task<Void, Never>?\n\n    init() {\n        LLVS.log.level = .verbose\n        let coordinator = try! StoreCoordinator()\n        let container = CKContainer(identifier: \"iCloud.com.mentalfaculty.themessage\")\n        let exchange = CloudKitExchange(with: coordinator.store, storeIdentifier: \"MainStore\", cloudDatabaseDescription: .publicDatabase(container))\n        coordinator.exchange = exchange\n        self.storeCoordinator = coordinator\n\n        versionTask = Task { [weak self] in\n            guard let self else { return }\n            for await _ in coordinator.currentVersionUpdates {\n                self.message = self.fetchMessage() ?? \"Let there be light!\"\n            }\n        }\n\n        startPolling()\n    }\n\n    private func fetchMessage() -> String? {\n        guard let value = try? storeCoordinator.value(id: messageId) else { return nil }\n        return String(data: value.data, encoding: .utf8)\n    }\n\n    func post(message: String) {\n        let data = message.data(using: .utf8)!\n        let newValue = Value(id: messageId, data: data)\n        try! storeCoordinator.save(updating: [newValue])\n        sync()\n    }\n\n    func sync() {\n        Task {\n            try? await storeCoordinator.exchange()\n            storeCoordinator.merge()\n        }\n    }\n\n    private func startPolling() {\n        pollingTask = Task {\n            while !Task.isCancelled {\n                try? await Task.sleep(for: .seconds(15))\n                try? await storeCoordinator.exchange()\n                storeCoordinator.merge()\n            }\n        }\n    }\n\n    deinit {\n        versionTask?.cancel()\n        pollingTask?.cancel()\n    }\n}\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Samples/TheMessage/TheMessage/TheMessage.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.icloud-container-identifiers</key>\n\t<array>\n\t\t<string>iCloud.com.mentalfaculty.themessage</string>\n\t</array>\n\t<key>com.apple.developer.icloud-services</key>\n\t<array>\n\t\t<string>CloudKit</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage/TheMessageApp.swift",
    "content": "import SwiftUI\n\n@main\nstruct TheMessageApp: App {\n    @State private var store = MessageStore()\n\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n                .environment(store)\n        }\n    }\n}\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t07E880462350EDE5003471B4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880452350EDE5003471B4 /* ContentView.swift */; };\n\t\t07E880482350EDE6003471B4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07E880472350EDE6003471B4 /* Assets.xcassets */; };\n\t\t07E8804B2350EDE6003471B4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */; };\n\t\t07E8805E2350EF64003471B4 /* LLVS in Frameworks */ = {isa = PBXBuildFile; productRef = 07E8805D2350EF64003471B4 /* LLVS */; };\n\t\t07E880602350EF64003471B4 /* LLVSCloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07E8805F2350EF64003471B4 /* LLVSCloudKit */; };\n\t\t07E880632350EF64003471B4 /* TheMessageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880612350EF64003471B4 /* TheMessageApp.swift */; };\n\t\t07E880642350EF64003471B4 /* MessageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880622350EF64003471B4 /* MessageStore.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t07E8803E2350EDE5003471B4 /* TheMessage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TheMessage.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t07E880452350EDE5003471B4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\t07E880472350EDE6003471B4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\t07E880552350EE21003471B4 /* TheMessage.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TheMessage.entitlements; sourceTree = \"<group>\"; };\n\t\t07E880612350EF64003471B4 /* TheMessageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheMessageApp.swift; sourceTree = \"<group>\"; };\n\t\t07E880622350EF64003471B4 /* MessageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStore.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t07E8803B2350EDE5003471B4 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t07E8805E2350EF64003471B4 /* LLVS in Frameworks */,\n\t\t\t\t07E880602350EF64003471B4 /* LLVSCloudKit in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t07E880352350EDE5003471B4 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t07E880402350EDE5003471B4 /* TheMessage */,\n\t\t\t\t07E8803F2350EDE5003471B4 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t07E8803F2350EDE5003471B4 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t07E8803E2350EDE5003471B4 /* TheMessage.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t07E880402350EDE5003471B4 /* TheMessage */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t07E880552350EE21003471B4 /* TheMessage.entitlements */,\n\t\t\t\t07E880612350EF64003471B4 /* TheMessageApp.swift */,\n\t\t\t\t07E880622350EF64003471B4 /* MessageStore.swift */,\n\t\t\t\t07E880452350EDE5003471B4 /* ContentView.swift */,\n\t\t\t\t07E880472350EDE6003471B4 /* Assets.xcassets */,\n\t\t\t\t07E880492350EDE6003471B4 /* Preview Content */,\n\t\t\t);\n\t\t\tpath = TheMessage;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t07E880492350EDE6003471B4 /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t07E8803D2350EDE5003471B4 /* TheMessage */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 07E880522350EDE6003471B4 /* Build configuration list for PBXNativeTarget \"TheMessage\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t07E8803A2350EDE5003471B4 /* Sources */,\n\t\t\t\t07E8803B2350EDE5003471B4 /* Frameworks */,\n\t\t\t\t07E8803C2350EDE5003471B4 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = TheMessage;\n\t\t\tpackageProductDependencies = (\n\t\t\t\t07E8805D2350EF64003471B4 /* LLVS */,\n\t\t\t\t07E8805F2350EF64003471B4 /* LLVSCloudKit */,\n\t\t\t);\n\t\t\tproductName = TheMessage;\n\t\t\tproductReference = 07E8803E2350EDE5003471B4 /* TheMessage.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t07E880362350EDE5003471B4 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 1600;\n\t\t\t\tLastUpgradeCheck = 2630;\n\t\t\t\tORGANIZATIONNAME = \"Momenta B.V.\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t07E8803D2350EDE5003471B4 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 11.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 07E880392350EDE5003471B4 /* Build configuration list for PBXProject \"TheMessage\" */;\n\t\t\tcompatibilityVersion = \"Xcode 14.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 07E880352350EDE5003471B4;\n\t\t\tpackageReferences = (\n\t\t\t\t07E880652350EF64003471B4 /* XCLocalSwiftPackageReference \"../..\" */,\n\t\t\t);\n\t\t\tproductRefGroup = 07E8803F2350EDE5003471B4 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t07E8803D2350EDE5003471B4 /* TheMessage */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t07E8803C2350EDE5003471B4 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t07E8804B2350EDE6003471B4 /* Preview Assets.xcassets in Resources */,\n\t\t\t\t07E880482350EDE6003471B4 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t07E8803A2350EDE5003471B4 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t07E880632350EF64003471B4 /* TheMessageApp.swift in Sources */,\n\t\t\t\t07E880642350EF64003471B4 /* MessageStore.swift in Sources */,\n\t\t\t\t07E880462350EDE5003471B4 /* ContentView.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\t07E880502350EDE6003471B4 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"$(inherited) DEBUG\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t07E880512350EDE6003471B4 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 18.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSTRING_CATALOG_GENERATE_SYMBOLS = YES;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t07E880532350EDE6003471B4 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = TheMessage/TheMessage.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"TheMessage/Preview Content\\\"\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.themessage;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t07E880542350EDE6003471B4 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = TheMessage/TheMessage.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"TheMessage/Preview Content\\\"\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.themessage;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 6.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t07E880392350EDE5003471B4 /* Build configuration list for PBXProject \"TheMessage\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t07E880502350EDE6003471B4 /* Debug */,\n\t\t\t\t07E880512350EDE6003471B4 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t07E880522350EDE6003471B4 /* Build configuration list for PBXNativeTarget \"TheMessage\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t07E880532350EDE6003471B4 /* Debug */,\n\t\t\t\t07E880542350EDE6003471B4 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCLocalSwiftPackageReference section */\n\t\t07E880652350EF64003471B4 /* XCLocalSwiftPackageReference \"../..\" */ = {\n\t\t\tisa = XCLocalSwiftPackageReference;\n\t\t\trelativePath = ../..;\n\t\t};\n/* End XCLocalSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\t07E8805D2350EF64003471B4 /* LLVS */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 07E880652350EF64003471B4 /* XCLocalSwiftPackageReference \"../..\" */;\n\t\t\tproductName = LLVS;\n\t\t};\n\t\t07E8805F2350EF64003471B4 /* LLVSCloudKit */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 07E880652350EF64003471B4 /* XCLocalSwiftPackageReference \"../..\" */;\n\t\t\tproductName = LLVSCloudKit;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = 07E880362350EDE5003471B4 /* Project object */;\n}\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:TheMessage.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Sources/LLVS/Core/Exchange.swift",
    "content": "//\n//  Exchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 25/02/2019.\n//\n\nimport Foundation\n\nenum ExchangeError: Swift.Error {\n    case remoteVersionsWithUnknownPredecessors\n    case missingVersion\n    case unknown(error: Swift.Error)\n}\n\npublic typealias VersionChanges = (version: Version, valueChanges: [Value.Change])\n\npublic protocol SnapshotExchange {\n    func retrieveSnapshotManifest() async throws -> SnapshotManifest?\n    func retrieveSnapshotChunk(index: Int) async throws -> Data\n    func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws\n}\n\npublic protocol Exchange: AnyObject {\n\n    var newVersionsAvailable: AsyncStream<Void> { get }\n\n    var store: Store { get }\n\n    var restorationState: Data? { get set }\n\n    func retrieve() async throws -> [Version.ID]\n    func prepareToRetrieve() async throws\n    func retrieveAllVersionIdentifiers() async throws -> [Version.ID]\n    func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version]\n    func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]]\n\n    func send() async throws -> [Version.ID]\n    func prepareToSend() async throws\n    func send(versionChanges: [VersionChanges]) async throws\n}\n\n// MARK:- Retrieving\n\npublic extension Exchange {\n\n    func retrieve() async throws -> [Version.ID] {\n        log.trace(\"Retrieving\")\n\n        try await self.prepareToRetrieve()\n\n        let remoteIds = try await self.retrieveAllVersionIdentifiers()\n\n        let toRetrieveIds = self.versionIdsMissingFromHistory(forRemoteIdentifiers: remoteIds)\n        log.verbose(\"Version identifiers to retrieve: \\(toRetrieveIds.idStrings)\")\n        let remoteVersions = try await self.retrieveVersions(identifiedBy: toRetrieveIds)\n\n        log.verbose(\"Adding to history versions: \\(remoteVersions.idStrings)\")\n        try await self.addToHistory(remoteVersions)\n\n        log.trace(\"Retrieved\")\n        return remoteIds\n    }\n\n    private func versionIdsMissingFromHistory(forRemoteIdentifiers remoteIdentifiers: [Version.ID]) -> [Version.ID] {\n        var toRetrieveIds: [Version.ID]!\n        self.store.queryHistory { history in\n            let storeVersionIds = Set(history.allVersionIdentifiers)\n            let remoteVersionIds = Set(remoteIdentifiers)\n            toRetrieveIds = Array(remoteVersionIds.subtracting(storeVersionIds))\n        }\n        return toRetrieveIds\n    }\n\n    private func addToHistory(_ versions: [Version]) async throws {\n        let versionsByIdentifier = versions.reduce(into: [:]) { result, version in\n            result[version.id] = version\n        }\n\n        let sortedVersions = versions.sorted { $0.timestamp < $1.timestamp }\n\n        func batchSizeCostEvaluator(index: Int) -> Float {\n            let batchDataSizeLimit = 5000000 // 5MB\n            let version = sortedVersions[index]\n            return Float(version.valueDataSize ?? 100000) / Float(batchDataSizeLimit)\n        }\n\n        let dynamicBatcher = DynamicTaskBatcher(numberOfTasks: sortedVersions.count, taskCostEvaluator: batchSizeCostEvaluator) { range in\n            let batchVersions = Array(sortedVersions[range])\n            let valueChangesByVersionIdentifier: [Version.ID: [Value.Change]]\n            do {\n                valueChangesByVersionIdentifier = try await self.retrieveValueChanges(forVersionsIdentifiedBy: batchVersions.ids)\n            } catch {\n                log.error(\"Failed adding to history: \\(error)\")\n                return .definitive(.failure(error))\n            }\n\n            let valueChangesByVersionID: [Version.ID: [Value.Change]] = valueChangesByVersionIdentifier.reduce(into: [:]) { result, keyValue in\n                var version = versionsByIdentifier[keyValue.key]!\n                if version.valueDataSize == nil { version.valueDataSize = keyValue.value.valueDataSize }\n                result[version.id] = keyValue.value\n            }\n\n            do {\n                try self.addToHistorySync(sortedVersions: batchVersions, valueChangesByVersionID: valueChangesByVersionID)\n                return .definitive(.success(()))\n            } catch let error as ExchangeError where error.isUnknownPredecessors {\n                return .growBatchAndReexecute\n            } catch {\n                return .definitive(.failure(error))\n            }\n        }\n        try await dynamicBatcher.start()\n    }\n\n    /// Synchronously add sorted versions to history, iterating until all appendable versions are consumed.\n    private func addToHistorySync(sortedVersions: [Version], valueChangesByVersionID: [Version.ID: [Value.Change]]) throws {\n        var remaining = sortedVersions\n        while !remaining.isEmpty {\n            guard let version = appendableVersion(from: remaining) else {\n                log.error(\"Failed to add to history due to missing predecessors\")\n                throw ExchangeError.remoteVersionsWithUnknownPredecessors\n            }\n            let valueChanges = valueChangesByVersionID[version.id]!\n            log.trace(\"Adding version to store: \\(version.id.rawValue)\")\n            log.verbose(\"Value changes for \\(version.id.rawValue): \\(valueChanges)\")\n\n            do {\n                try self.store.addVersion(version, storing: valueChanges)\n            } catch Store.Error.attemptToAddExistingVersion {\n                log.error(\"Failed adding to history because version already exists. Ignoring error\")\n            }\n\n            remaining = remaining.filter { $0.id != version.id }\n        }\n    }\n\n    private func appendableVersion(from versions: [Version]) -> Version? {\n        return versions.first { v in\n            return store.historyIncludesVersions(identifiedBy: v.predecessors?.ids ?? [])\n        }\n    }\n\n}\n\n// MARK:- Sending\n\npublic extension Exchange {\n\n    func send() async throws -> [Version.ID] {\n        try await self.prepareToSend()\n\n        let remoteIds = try await self.retrieveAllVersionIdentifiers()\n\n        let toSendIds = self.versionIdsMissingRemotely(forRemoteIdentifiers: remoteIds)\n\n        func batchSizeCostEvaluator(index: Int) -> Float {\n            let batchDataSizeLimit: Int64 = 5000000 // 5MB\n            let defaultDataSize: Int64 = 100000 // 100KB\n            if let version = try? self.store.version(identifiedBy: toSendIds[index]) {\n                return Float(version.valueDataSize ?? defaultDataSize) / Float(batchDataSizeLimit)\n            } else {\n                return Float(defaultDataSize) / Float(batchDataSizeLimit)\n            }\n        }\n\n        let taskBatcher = DynamicTaskBatcher(numberOfTasks: toSendIds.count, taskCostEvaluator: batchSizeCostEvaluator) { range in\n            do {\n                let versionChanges: [VersionChanges] = try toSendIds[range].map { versionId in\n                    guard let version = try self.store.version(identifiedBy: versionId) else {\n                        throw ExchangeError.missingVersion\n                    }\n                    let changes = try self.store.valueChanges(madeInVersionIdentifiedBy: versionId)\n                    return (version, changes)\n                }\n\n                guard !versionChanges.isEmpty else {\n                    return .definitive(.success(()))\n                }\n\n                try await self.send(versionChanges: versionChanges)\n                return .definitive(.success(()))\n            } catch {\n                return .definitive(.failure(error))\n            }\n        }\n        try await taskBatcher.start()\n\n        return toSendIds\n    }\n\n    private func versionIdsMissingRemotely(forRemoteIdentifiers remoteIdentifiers: [Version.ID]) -> [Version.ID] {\n        var toSendIds: [Version.ID]!\n        self.store.queryHistory { history in\n            let storeVersionIds = Set(history.allVersionIdentifiers)\n            let remoteVersionIds = Set(remoteIdentifiers)\n            toSendIds = Array(storeVersionIds.subtracting(remoteVersionIds))\n        }\n        return toSendIds\n    }\n}\n\n// MARK:- ExchangeError helper\n\nfileprivate extension ExchangeError {\n    var isUnknownPredecessors: Bool {\n        if case .remoteVersionsWithUnknownPredecessors = self { return true }\n        return false\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/FileZone.swift",
    "content": "//\n//  FileZone.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\n\npublic class FileStorage: Storage, SnapshotCapable {\n\n    private let fileExtension = \"json\"\n\n    public init() {}\n\n    public func makeMapZone(for type: MapType, in store: Store) -> Zone {\n        switch type {\n        case .valuesByVersion:\n            return FileZone(rootDirectory: store.valuesMapDirectoryURL, fileExtension: fileExtension)\n        case .userDefined:\n            fatalError(\"User defined maps not yet supported\")\n        }\n    }\n\n    public func makeValuesZone(in store: Store) -> Zone {\n        return FileZone(rootDirectory: store.valuesDirectoryURL, fileExtension: fileExtension)\n    }\n\n}\n\ninternal final class FileZone: Zone {\n    \n    let rootDirectory: URL\n    let fileExtension: String\n    \n    private let uncachableDataSizeLimit = 10000 // 10KB\n    private let cache: Cache<Data> = .init()\n    \n    fileprivate let fileManager = FileManager()\n    \n    init(rootDirectory: URL, fileExtension: String) {\n        self.rootDirectory = rootDirectory.resolvingSymlinksInPath()\n        try? fileManager.createDirectory(at: rootDirectory, withIntermediateDirectories: true, attributes: nil)\n        self.fileExtension = fileExtension\n    }\n    \n    private func cacheIfNeeded(_ data: Data, for reference: ZoneReference) {\n        if data.count < uncachableDataSizeLimit {\n            cache.setValue(data, for: reference)\n        }\n    }\n    \n    internal func store(_ data: Data, for reference: ZoneReference) throws {\n        let (dir, file) = try fileSystemLocation(for: reference)\n        try? fileManager.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)\n        let compressed = DataCompression.compress(data)\n        try compressed.write(to: file)\n        cacheIfNeeded(data, for: reference)\n    }\n\n    internal func data(for reference: ZoneReference) throws -> Data? {\n        if let data = cache.value(for: reference) { return data }\n        let (_, file) = try fileSystemLocation(for: reference)\n        guard let raw = try? Data(contentsOf: file) else { return nil }\n        let data = DataCompression.decompressIfNeeded(raw)\n        cacheIfNeeded(data, for: reference)\n        return data\n    }\n    \n    func fileSystemLocation(for reference: ZoneReference) throws -> (directoryURL: URL, fileURL: URL) {\n        let safeKey = reference.key.replacingOccurrences(of: \"/\", with: \"LLVSSLASH\").replacingOccurrences(of: \":\", with: \"LLVSCOLON\")\n        let valueDirectoryURL = rootDirectory.appendingSplitPathComponent(safeKey)\n        let versionName = reference.version.rawValue + \".\" + fileExtension\n        let fileURL = valueDirectoryURL.appendingSplitPathComponent(versionName, prefixLength: 1)\n        let directoryURL = fileURL.deletingLastPathComponent()\n        return (directoryURL: directoryURL, fileURL: fileURL)\n    }\n    \n    internal func versionIds(for key: String) throws -> [Version.ID] {\n        let valueDirectoryURL = rootDirectory.appendingSplitPathComponent(key)\n        let valueDirLength = valueDirectoryURL.path.count\n        let enumerator = fileManager.enumerator(at: valueDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!\n        var versions: [Version.ID] = []\n        let slash = Character(\"/\")\n        for any in enumerator {\n            var isDirectory: ObjCBool = true\n            guard let url = any as? URL else { continue }\n            guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }\n            let path = url.resolvingSymlinksInPath().deletingPathExtension().path\n            let index = path.index(path.startIndex, offsetBy: Int(valueDirLength))\n            let version = String(path[index...]).filter { $0 != slash }\n            versions.append(Version.ID(version))\n        }\n        return versions\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/FolderBasedExchange.swift",
    "content": "//\n//  FolderBasedExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\npublic enum FolderBasedExchangeError: Error {\n    case versionFileInvalid\n    case fileNotFound(String)\n    case foldersNotInitialized\n}\n\npublic protocol FolderBasedExchange: Exchange {\n    associatedtype FileID\n    associatedtype FolderID\n\n    var versionsFolderID: FolderID? { get }\n    var changesFolderID: FolderID? { get }\n\n    func notifyNewVersionsAvailable()\n    func listFiles(inFolder folder: FolderID) async throws -> [String: FileID]\n    func downloadData(forFile fileID: FileID) async throws -> Data\n    func uploadData(_ data: Data, named name: String, toFolder folder: FolderID) async throws\n}\n\npublic extension FolderBasedExchange {\n\n    func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        guard let folderID = versionsFolderID else { return [] }\n        let fileMap = try await listFiles(inFolder: folderID)\n        return fileMap.map { Version.ID($0.key) }\n    }\n\n    func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        guard !versionIds.isEmpty else { return [] }\n        guard let folderID = versionsFolderID else { return [] }\n\n        let fileMap = try await listFiles(inFolder: folderID)\n        var versions: [Version] = []\n        for versionId in versionIds {\n            guard let fileID = fileMap[versionId.rawValue] else {\n                throw FolderBasedExchangeError.fileNotFound(versionId.rawValue)\n            }\n            let data = try await downloadData(forFile: fileID)\n            if let version = try JSONDecoder().decode([String: Version].self, from: data)[\"version\"] {\n                versions.append(version)\n            } else {\n                throw FolderBasedExchangeError.versionFileInvalid\n            }\n        }\n        return versions\n    }\n\n    func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        guard !versionIds.isEmpty else { return [:] }\n        guard let folderID = changesFolderID else { return [:] }\n\n        let fileMap = try await listFiles(inFolder: folderID)\n        var changesByVersion: [Version.ID: [Value.Change]] = [:]\n        for versionId in versionIds {\n            guard let fileID = fileMap[versionId.rawValue] else {\n                throw FolderBasedExchangeError.fileNotFound(versionId.rawValue)\n            }\n            let data = try await downloadData(forFile: fileID)\n            let changes = try JSONDecoder().decode([Value.Change].self, from: data)\n            changesByVersion[versionId] = changes\n        }\n        return changesByVersion\n    }\n\n    func send(versionChanges: [VersionChanges]) async throws {\n        guard !versionChanges.isEmpty else { return }\n        guard let versionsFolderID = versionsFolderID,\n              let changesFolderID = changesFolderID else {\n            throw FolderBasedExchangeError.foldersNotInitialized\n        }\n\n        for (version, valueChanges) in versionChanges {\n            let changesData = try JSONEncoder().encode(valueChanges)\n            let versionData = try JSONEncoder().encode([\"version\": version])\n\n            try await uploadData(changesData, named: version.id.rawValue, toFolder: changesFolderID)\n            try await uploadData(versionData, named: version.id.rawValue, toFolder: versionsFolderID)\n        }\n\n        notifyNewVersionsAvailable()\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/History.swift",
    "content": "//\n//  History.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 11/11/2018.\n//\n\nimport Foundation\n\npublic class History {\n    \n    public enum Error: Swift.Error {\n        case attemptToAddPreexistingVersion(id: String)\n        case nonExistentVersionEncountered(identifier: String)\n    }\n    \n    private var versionsByIdentifier: [Version.ID:Version] = [:]\n    private var referencedVersionIdentifiers: Set<Version.ID> = [] // Any version that is a predecessor\n    \n    public private(set) var headIdentifiers: Set<Version.ID> = []  // Versions that are not predecessors of other versions\n    public var allVersionIdentifiers: [Version.ID] { return Array(versionsByIdentifier.keys) }\n    \n    public var mostRecentHead: Version? {\n        let maxId = headIdentifiers.max { (vId1, vId2) -> Bool in\n            let v1 = version(identifiedBy: vId1)!\n            let v2 = version(identifiedBy: vId2)!\n            return v1.timestamp < v2.timestamp\n        }\n        return maxId.flatMap { version(identifiedBy: $0) }\n    }\n    \n    public func version(identifiedBy identifier: Version.ID) -> Version? {\n        return versionsByIdentifier[identifier]\n    }\n    \n    internal func version(prevailingFromCandidates candidates: [Version.ID], at versionId: Version.ID) -> Version? {\n        if let candidate = candidates.first(where: { $0 == versionId }) {\n            return version(identifiedBy: candidate)\n        }\n        \n        var ancestors: Set<Version.ID> = [versionId]\n        for v in self {\n            // See if v is in our ancestry. If so, extend ancestry.\n            if ancestors.contains(v.id) {\n                ancestors.formUnion(v.predecessors?.ids ?? [])\n                ancestors.remove(v.id)\n            }\n            \n            if let candidate = candidates.first(where: { ancestors.contains($0) }) {\n                return version(identifiedBy: candidate)\n            }\n        }\n        \n        return nil\n    }\n    \n    internal func isAncestralLine(from ancestor: Version.ID, to descendant: Version.ID) -> Bool {\n         return nil != version(prevailingFromCandidates: [ancestor], at: descendant)\n    }\n    \n    /// If updatingPredecessorVersions is true, the successors of other versions may be updated.\n    /// Use this when adding a new head when storing.\n    /// Pass in false if more control is needed over setting the successors, such as\n    /// when loading them to setup the History. In that case, we only want to set them when all versions\n    /// have been loaded.\n    internal func add(_ version: Version, updatingPredecessorVersions: Bool) throws {\n        guard versionsByIdentifier[version.id] == nil else {\n            throw Error.attemptToAddPreexistingVersion(id: version.id.rawValue)\n        }\n        \n        versionsByIdentifier[version.id] = version\n        if updatingPredecessorVersions {\n            try updateSuccessors(inPredecessorsOf: version)\n        }\n        \n        if !referencedVersionIdentifiers.contains(version.id) {\n            headIdentifiers.insert(version.id)\n        }\n    }\n    \n    internal func updateSuccessors(inPredecessorsOf version: Version) throws {\n        for predecessorIdentifier in version.predecessors?.ids ?? [] {\n            guard let predecessor = self.version(identifiedBy: predecessorIdentifier) else {\n                throw Error.nonExistentVersionEncountered(identifier: predecessorIdentifier.rawValue)\n            }\n            \n            referencedVersionIdentifiers.insert(predecessorIdentifier)\n            headIdentifiers.remove(predecessorIdentifier)\n            \n            var newPredecessor = predecessor\n            let newSuccessorIdentifiers = predecessor.successors.ids.union([version.id])\n            newPredecessor.successors = Version.Successors(ids: newSuccessorIdentifiers)\n            versionsByIdentifier[newPredecessor.id] = newPredecessor\n        }\n    }\n    \n    /// Finds the greatest common ancestor of all the given version IDs by pairwise reduction.\n    internal func greatestCommonAncestor(ofAll versionIds: Set<Version.ID>) throws -> Version.ID? {\n        guard !versionIds.isEmpty else { return nil }\n        var ids = Array(versionIds)\n        var result = ids.removeFirst()\n        for id in ids {\n            guard let gca = try greatestCommonAncestor(ofVersionsIdentifiedBy: (result, id)) else {\n                return nil\n            }\n            result = gca\n        }\n        return result\n    }\n\n    public func greatestCommonAncestor(ofVersionsIdentifiedBy ids: (Version.ID, Version.ID)) throws -> Version.ID? {\n        // Find all ancestors of first Version. Determine how many generations back each Version is.\n        // We take the shortest path to any given Version, ie, the minimum of possible paths.\n        var generationById = [Version.ID:Int]()\n        var firstFront: Set<Version.ID> = [ids.0]\n        \n        func propagateFront(front: inout Set<Version.ID>) throws {\n            var newFront = Set<Version.ID>()\n            for identifier in front {\n                guard let frontVersion = self.version(identifiedBy: identifier) else {\n                    throw Error.nonExistentVersionEncountered(identifier: identifier.rawValue)\n                }\n                newFront.formUnion(frontVersion.predecessors?.ids ?? [])\n            }\n            front = newFront\n        }\n        \n        var generation = 0\n        while firstFront.count > 0 {\n            firstFront.forEach { generationById[$0] = Swift.min(generationById[$0] ?? Int.max, generation) }\n            try propagateFront(front: &firstFront)\n            generation += 1\n        }\n        \n        // Now go through ancestors of second version until we find the first in common with the first ancestors\n        var secondFront: Set<Version.ID> = [ids.1]\n        let ancestorsOfFirst = Set(generationById.keys)\n        while secondFront.count > 0 {\n            let common = ancestorsOfFirst.intersection(secondFront)\n            let sorted = common.sorted { generationById[$0]! < generationById[$1]! }\n            if let mostRecentCommon = sorted.first { return mostRecentCommon }\n            try propagateFront(front: &secondFront)\n        }\n        \n        return nil\n    }\n}\n\n\nextension History: Sequence {\n    \n    /// Enumerates history in a topological sorted order.\n    /// Note that there are many possible orders that satisfy this.\n    /// Most recent versions are ordered first (ie heads).\n    /// Return false from block to stop.\n    /// Uses Kahn algorithm to generate the order. https://en.wikipedia.org/wiki/Topological_sorting\n    public struct TopologicalIterator: IteratorProtocol {\n        public typealias Element = Version\n        \n        public let history: History\n        \n        private var front: Set<Version>\n        private var referenceCountByIdentifier: [Version.ID:Int] = [:]\n        \n        init(toIterate history: History) {\n            self.history = history\n            let headVersions = history.headIdentifiers.map {\n                history.version(identifiedBy: $0)!\n            }\n            self.front = Set(headVersions)\n        }\n        \n        public mutating func next() -> Version? {\n            guard let next = front.first(where: { version in\n                    let refCount = self.referenceCountByIdentifier[version.id] ?? 0\n                    let successorCount = version.successors.ids.count\n                    return refCount == successorCount\n                })\n                else {\n                    return nil\n                }\n            \n            for predecessorIdentifier in next.predecessors?.ids ?? [] {\n                let predecessor = history.version(identifiedBy: predecessorIdentifier)!\n                referenceCountByIdentifier[predecessor.id] = (referenceCountByIdentifier[predecessor.id] ?? 0) + 1\n                front.insert(predecessor)\n            }\n            \n            front.remove(next)\n            return next\n        }\n    }\n    \n    public func makeIterator() -> History.TopologicalIterator {\n        return Iterator(toIterate: self)\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Map.swift",
    "content": "//\n//  Map.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 30/11/2018.\n//\n\nimport Foundation\n\n\nfinal class Map {\n    \n    enum Error: Swift.Error {\n        case encodingFailure(String)\n        case unexpectedNodeContent\n        case missingNode\n        case missingVersionRoot\n    }\n        \n    let zone: Zone\n    private let nodeCache: Cache<Node> = .init()\n    \n    private let rootKey = \"__llvs_root\"\n    \n    init(zone: Zone) {\n        self.zone = zone\n    }\n    \n    func addVersion(_ version: Version.ID, basedOn baseVersion: Version.ID?, applying deltas: [Delta]) throws {\n        try autoreleasepool {\n            let encoder = JSONEncoder()\n            var rootNode: Node\n            let rootRef = ZoneReference(key: rootKey, version: version)\n            if let baseVersion = baseVersion {\n                let oldRootRef = ZoneReference(key: rootKey, version: baseVersion)\n                guard let oldRoot = try node(for: oldRootRef) else { throw Error.missingNode }\n                rootNode = oldRoot\n            }\n            else {\n                rootNode = Node(reference: rootRef, children: .nodes([]))\n            }\n            rootNode.reference.version = version\n            guard case let .nodes(rootChildRefs) = rootNode.children else { throw Error.unexpectedNodeContent }\n            \n            var subNodesByKey: [Key:Node] = [:]\n            for delta in deltas {\n                let key = delta.key\n                \n                let subNodeKey = Key(String(key.keyString.prefix(2)))\n                let subNodeRef = ZoneReference(key: subNodeKey.keyString, version: version)\n                var subNode: Node\n                if let n = subNodesByKey[subNodeKey] {\n                    subNode = n\n                }\n                else if let existingSubNodeRef = rootChildRefs.first(where: { $0.key == subNodeKey.keyString }) {\n                    guard let existingSubNode = try node(for: existingSubNodeRef) else { throw Error.missingNode }\n                    subNode = existingSubNode\n                    subNode.reference = subNodeRef\n                }\n                else {\n                    subNode = Node(reference: subNodeRef, children: .values([]))\n                }\n                \n                guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }\n                \n                let valueRefs = keyValuePairs.filter({ $0.key == key }).map({ $0.valueReference })\n                var valueRefsByIdentifier: [Value.ID:Value.Reference] = Dictionary(uniqueKeysWithValues: valueRefs.map({ ($0.valueId, $0) }) )\n                for valueRef in delta.addedValueReferences {\n                    valueRefsByIdentifier[valueRef.valueId] = valueRef\n                }\n                for valueId in delta.removedValueIdentifiers {\n                    valueRefsByIdentifier[valueId] = nil\n                }\n                let newValueRefs = Array(valueRefsByIdentifier.values)\n                var newPairs = keyValuePairs.filter { $0.key != key }\n                newPairs += newValueRefs.map { KeyValuePair(key: key, valueReference: $0) }\n                subNode.children = .values(newPairs)\n                \n                subNodesByKey[subNodeKey] = subNode\n            }\n            \n            // Update and save subnodes and rootnode\n            var rootRefsByIdentifier: [String:ZoneReference] = Dictionary(uniqueKeysWithValues: rootChildRefs.map({ ($0.key, $0) }) )\n            for subNode in subNodesByKey.values {\n                let key = subNode.reference.key\n                let data = try encoder.encode(subNode)\n                try zone.store(data, for: subNode.reference)\n                rootRefsByIdentifier[key] = subNode.reference\n            }\n            rootNode.children = .nodes(Array(rootRefsByIdentifier.values))\n            let data = try encoder.encode(rootNode)\n            try zone.store(data, for: rootNode.reference)\n        }\n    }\n    \n    func purgeCache() {\n        nodeCache.purgeAllValues()\n    }\n\n    /// Returns zone references for the root node and all subnodes for a given version.\n    /// Used during compaction cleanup to know what Map data to delete.\n    func zoneReferences(forVersionIdentifiedBy versionId: Version.ID) throws -> [ZoneReference] {\n        let rootRef = ZoneReference(key: rootKey, version: versionId)\n        guard let rootNode = try node(for: rootRef) else { return [] }\n        var refs: [ZoneReference] = [rootRef]\n        if case let .nodes(subNodeRefs) = rootNode.children {\n            refs.append(contentsOf: subNodeRefs)\n        }\n        return refs\n    }\n\n    func differences(between firstVersion: Version.ID, and secondVersion: Version.ID, withCommonAncestor commonAncestor: Version.ID?) throws -> [Diff] {\n        let originRef = commonAncestor.flatMap { ZoneReference(key: rootKey, version: $0) }\n        let rootRef1 = ZoneReference(key: rootKey, version: firstVersion)\n        let rootRef2 = ZoneReference(key: rootKey, version: secondVersion)\n        \n        let originNode = try originRef.flatMap { try node(for: $0) }\n        guard\n            let rootNode1 = try node(for: rootRef1),\n            let rootNode2 = try node(for: rootRef2) else {\n            throw Error.missingVersionRoot\n        }\n\n        let nodesOrigin: [ZoneReference]?\n        if case let .nodes(n)? = originNode?.children {\n            nodesOrigin = n\n        } else {\n            nodesOrigin = nil\n        }\n        guard\n            case let .nodes(subNodes1) = rootNode1.children,\n            case let .nodes(subNodes2) = rootNode2.children else {\n            throw Error.unexpectedNodeContent\n        }\n\n        let refOriginByKey: [String:ZoneReference]? = nodesOrigin.flatMap { refs in .init(uniqueKeysWithValues: refs.map({ ($0.key, $0) })) }\n        let subNodeRefs1ByKey: [String:ZoneReference] = .init(uniqueKeysWithValues: subNodes1.map({ ($0.key, $0) }))\n        let subNodeRefs2ByKey: [String:ZoneReference] = .init(uniqueKeysWithValues: subNodes2.map({ ($0.key, $0) }))\n        var allSubNodeKeys = Set(subNodeRefs1ByKey.keys).union(subNodeRefs2ByKey.keys)\n        if let r = refOriginByKey { allSubNodeKeys.formUnion(r.keys) }\n\n        // Batch prefetch only subnodes that will actually be compared\n        var refsToFetch: [ZoneReference] = []\n        for subNodeKey in allSubNodeKeys {\n            let r1 = subNodeRefs1ByKey[subNodeKey]\n            let r2 = subNodeRefs2ByKey[subNodeKey]\n            if r1 == r2 { continue }\n            if let r = r1 { refsToFetch.append(r) }\n            if let r = r2 { refsToFetch.append(r) }\n            if let r = refOriginByKey?[subNodeKey] { refsToFetch.append(r) }\n        }\n        try prefetchNodes(for: refsToFetch)\n\n        var diffs: [Diff] = []\n        for subNodeKey in allSubNodeKeys {\n            \n            func appendDiffs(forIdentifiers ids: [Value.ID], fork: Value.Fork) throws {\n                for id in ids {\n                    let diff = Diff(key: .init(subNodeKey), valueId: id, valueFork: fork)\n                    diffs.append(diff)\n                }\n            }\n            \n            func appendDiffs(forSubNode subNodeRef: ZoneReference, fork: Value.Fork) throws {\n                let refs = try valueReferences(forRootSubNode: subNodeRef)\n                try appendDiffs(forIdentifiers: refs.map({ $0.valueId }), fork: fork)\n            }\n            \n            func appendDiffs(forOriginNode originNode: ZoneReference, onlyBranchNode branchNode: ZoneReference, branch: Value.Fork.Branch) throws {\n                let vo = try valueReferences(forRootSubNode: originNode)\n                let vb = try valueReferences(forRootSubNode: branchNode)\n                let refOById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vo.map({ ($0.valueId, $0) }))\n                let refBById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vb.map({ ($0.valueId, $0) }))\n                let allIds = Set(refOById.keys).union(refBById.keys)\n                for valueId in allIds {\n                    let refO = refOById[valueId]\n                    let refB = refBById[valueId]\n                    switch (refO, refB) {\n                    case let (ro?, rb?):\n                        if ro != rb {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: branch.opposite))\n                        } else {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removed(branch.opposite))\n                        }\n                    case (_?, nil):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .twiceRemoved)\n                    case (nil, _?):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .inserted(branch))\n                    case (nil, nil):\n                        fatalError()\n                    }\n                }\n            }\n            \n            let ref1 = subNodeRefs1ByKey[subNodeKey]\n            let ref2 = subNodeRefs2ByKey[subNodeKey]\n\n            // Skip bucket entirely if both branches reference the same subnode\n            if ref1 == ref2 { continue }\n\n            let origin = refOriginByKey?[subNodeKey]\n            switch (origin, ref1, ref2) {\n            case let (o?, r1?, r2?):\n                let vo = try valueReferences(forRootSubNode: o)\n                let v1 = try valueReferences(forRootSubNode: r1)\n                let v2 = try valueReferences(forRootSubNode: r2)\n                let refOById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vo.map({ ($0.valueId, $0) }))\n                let ref1ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v1.map({ ($0.valueId, $0) }))\n                let ref2ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v2.map({ ($0.valueId, $0) }))\n                let allIds = Set(refOById.keys).union(ref1ById.keys).union(ref2ById.keys)\n                for valueId in allIds {\n                    let refO = refOById[valueId]\n                    let ref1 = ref1ById[valueId]\n                    let ref2 = ref2ById[valueId]\n                    switch (refO, ref1, ref2) {\n                    case let (ro?, r1?, r2?):\n                        if ro == r1, ro != r2 {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .updated(.second))\n                        } else if ro != r1, ro == r2 {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .updated(.first))\n                        } else if ro != r1, ro != r2 {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .twiceUpdated)\n                        }\n                    case let (ro?, r1?, nil):\n                        if ro != r1 {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: .second))\n                        } else {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removed(.second))\n                        }\n                    case let (ro?, nil, r2?):\n                        if ro != r2 {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: .first))\n                        } else {\n                            try appendDiffs(forIdentifiers: [valueId], fork: .removed(.first))\n                        }\n                    case (nil, _?, _?):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .twiceInserted)\n                    case (nil, nil, _?):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.second))\n                    case (nil, _?, nil):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.first))\n                    case (_?, nil, nil):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .twiceRemoved)\n                    case (nil, nil, nil):\n                        fatalError()\n                    }\n                }\n            case let (o?, r1?, nil):\n                try appendDiffs(forOriginNode: o, onlyBranchNode: r1, branch: .first)\n            case let (o?, nil, r2?):\n                try appendDiffs(forOriginNode: o, onlyBranchNode: r2, branch: .second)\n            case let (nil, r1?, r2?):\n                let v1 = try valueReferences(forRootSubNode: r1)\n                let v2 = try valueReferences(forRootSubNode: r2)\n                let ref1ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v1.map({ ($0.valueId, $0) }))\n                let ref2ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v2.map({ ($0.valueId, $0) }))\n                let allIds = Set(ref1ById.keys).union(ref2ById.keys)\n                for valueId in allIds {\n                    let ref1 = ref1ById[valueId]\n                    let ref2 = ref2ById[valueId]\n                    switch (ref1, ref2) {\n                    case (_?, _?):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .twiceInserted)\n                    case (_?, nil):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.first))\n                    case (nil, _?):\n                        try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.second))\n                    case (nil, nil):\n                        fatalError()\n                    }\n                }\n            case let (nil, r1?, nil):\n                try appendDiffs(forSubNode: r1, fork: .inserted(.first))\n            case let (nil, nil, r2?):\n                try appendDiffs(forSubNode: r2, fork: .inserted(.second))\n            case let (o?, nil, nil):\n                try appendDiffs(forSubNode: o, fork: .twiceRemoved)\n            case (nil, nil, nil):\n                fatalError()\n            }\n        }\n\n        return diffs\n    }\n    \n    func enumerateValueReferences(forVersionIdentifiedBy versionId: Version.ID, executingForEach block: (Value.Reference) throws -> Void) throws {\n        let rootRef = ZoneReference(key: rootKey, version: versionId)\n        guard let rootNode = try node(for: rootRef) else { throw Error.missingVersionRoot }\n        guard case let .nodes(subNodeRefs) = rootNode.children else { throw Error.missingNode }\n        try prefetchNodes(for: subNodeRefs)\n        for subNodeRef in subNodeRefs {\n            guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }\n            guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }\n            for keyValuePair in keyValuePairs {\n                try block(keyValuePair.valueReference)\n            }\n        }\n    }\n    \n    func valueReferences(matching key: Map.Key, at version: Version.ID) throws -> [Value.Reference] {\n        let rootRef = ZoneReference(key: rootKey, version: version)\n        guard let rootNode = try node(for: rootRef) else { throw Error.missingVersionRoot }\n        guard case let .nodes(subNodeRefs) = rootNode.children else { throw Error.missingNode }\n        let subNodeKey = String(key.keyString.prefix(2))\n        guard let subNodeRef = subNodeRefs.first(where: { $0.key == subNodeKey }) else { return [] }\n        guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }\n        guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }\n        return keyValuePairs.filter({ $0.key == key }).map({ $0.valueReference })\n    }\n\n    fileprivate func node(for key: String, version: Version.ID) throws -> Node? {\n        let ref = ZoneReference(key: key, version: version)\n        return try node(for: ref)\n    }\n    \n    fileprivate func node(for reference: ZoneReference) throws -> Node? {\n        try autoreleasepool {\n            if let node = nodeCache.value(for: reference) {\n                return node\n            } else if let data = try zone.data(for: reference) {\n                let node = try JSONDecoder().decode(Node.self, from: data)\n                nodeCache.setValue(node, for: reference)\n                return node\n            } else {\n                return nil\n            }\n        }\n    }\n    \n    private func prefetchNodes(for references: [ZoneReference]) throws {\n        let uncachedRefs = references.filter { nodeCache.value(for: $0) == nil }\n        guard !uncachedRefs.isEmpty else { return }\n        let dataArray = try zone.data(for: uncachedRefs)\n        let decoder = JSONDecoder()\n        for (ref, data) in zip(uncachedRefs, dataArray) {\n            if let data = data, let node = try? decoder.decode(Node.self, from: data) {\n                nodeCache.setValue(node, for: ref)\n            }\n        }\n    }\n\n    private func valueReferences(forRootSubNode subNodeRef: ZoneReference) throws -> [Value.Reference] {\n        guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }\n        guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }\n        return keyValuePairs.map({ $0.valueReference })\n    }\n\n}\n\n\n// MARK:- Subtypes\n\nextension Map {\n    \n    struct Key: Codable, Hashable {\n        var keyString: String\n        init(_ keyString: String = UUID().uuidString) {\n            self.keyString = keyString\n        }\n    }\n    \n    struct Diff {\n        var key: Key\n        var valueId: Value.ID\n        var valueFork: Value.Fork\n    }\n    \n    struct KeyValuePair: Codable, Hashable {\n        var key: Key\n        var valueReference: Value.Reference\n    }\n    \n    struct Delta {\n        var key: Key\n        var addedValueReferences: [Value.Reference] = []\n        var removedValueIdentifiers: [Value.ID] = []\n        \n        init(key: Key) {\n            self.key = key\n        }\n    }\n    \n    struct Node: Codable, Hashable {\n        var reference: ZoneReference\n        var children: Children\n    }\n    \n    enum Children: Codable, Hashable {\n        case values([KeyValuePair])\n        case nodes([ZoneReference])\n        \n        enum Key: CodingKey {\n            case values\n            case nodes\n        }\n        \n        init(from decoder: Decoder) throws {\n            let container = try decoder.container(keyedBy: Key.self)\n            if let values = try? container.decode([KeyValuePair].self, forKey: .values) {\n                self = .values(values)\n            } else if let nodes = try? container.decode([ZoneReference].self, forKey: .nodes) {\n                self = .nodes(nodes)\n            } else {\n                throw Error.encodingFailure(\"No valid references found in decoder\")\n            }\n        }\n        \n        func encode(to encoder: Encoder) throws {\n            var container = encoder.container(keyedBy: Key.self)\n            switch self {\n            case let .values(values):\n                try container.encode(values, forKey: .values)\n            case let .nodes(nodes):\n                try container.encode(nodes, forKey: .nodes)\n            }\n        }\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Merge.swift",
    "content": "//\n//  Merge.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 19/12/2018.\n//\n\nimport Foundation\n\npublic struct Merge {\n    \n    public var commonAncestor: Version?\n    public var versions: (first: Version, second: Version)\n    public var forksByValueIdentifier: [Value.ID:Value.Fork] = [:]\n    \n    public init(versions: (first: Version, second: Version), commonAncestor: Version?) {\n        self.commonAncestor = commonAncestor\n        self.versions = versions\n    }\n\n}\n\npublic protocol MergeArbiter {\n    \n    func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change]\n    \n}\n\n/// When conflicting, always favor the branch with the most recent version.\npublic class MostRecentBranchFavoringArbiter: MergeArbiter {\n    \n    public init() {}\n\n    public func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change] {\n        let v = merge.versions\n        let favoredBranch: Value.Fork.Branch = v.first.timestamp >= v.second.timestamp ? .first : .second\n        let favoredVersion = favoredBranch == .first ? v.first : v.second\n        var changes: [Value.Change] = []\n        for (valueId, fork) in merge.forksByValueIdentifier {\n            switch fork {\n            case let .removedAndUpdated(removeBranch):\n                if removeBranch == favoredBranch {\n                    changes.append(.preserveRemoval(valueId))\n                } else {\n                    let value = try store.value(id: valueId, at: favoredVersion.id)!\n                    changes.append(.preserve(value.reference!))\n                }\n            case .twiceInserted, .twiceUpdated:\n                let value = try store.value(id: valueId, at: favoredVersion.id)!\n                changes.append(.preserve(value.reference!))\n            case .inserted, .removed, .updated, .twiceRemoved:\n                break\n            }\n        }\n        return changes\n    }\n    \n}\n\n/// Favors the most recent change on a conflict by conflict basis.\n/// Will pick an update over a removal, regardless of recency.\npublic class MostRecentChangeFavoringArbiter: MergeArbiter {\n    \n    public init() {}\n    \n    public func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change] {\n        let v = merge.versions\n        var changes: [Value.Change] = []\n        for (valueId, fork) in merge.forksByValueIdentifier {\n            switch fork {\n            case let .removedAndUpdated(removeBranch):\n                let favoredVersion = removeBranch.opposite == .first ? v.first : v.second\n                let value = try store.value(id: valueId, at: favoredVersion.id)!\n                changes.append(.preserve(value.reference!))\n            case .twiceInserted, .twiceUpdated:\n                let value1 = try store.value(id: valueId, at: v.first.id)!\n                var version1: Version!\n                store.queryHistory { version1 = $0.version(identifiedBy: value1.storedVersionId!) }\n                \n                let value2 = try store.value(id: valueId, at: v.second.id)!\n                var version2: Version!\n                store.queryHistory { version2 = $0.version(identifiedBy: value2.storedVersionId!) }\n                \n                if version1.timestamp >= version2.timestamp {\n                    changes.append(.preserve(value1.reference!))\n                } else {\n                    changes.append(.preserve(value2.reference!))\n                }\n            case .inserted, .removed, .updated, .twiceRemoved:\n                break\n            }\n        }\n        return changes\n    }\n    \n}\n\n"
  },
  {
    "path": "Sources/LLVS/Core/Snapshot.swift",
    "content": "//\n//  Snapshot.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 09/02/2026.\n//\n\nimport Foundation\n\n/// Metadata describing a snapshot stored in the cloud.\npublic struct SnapshotManifest: Codable {\n    public var snapshotId: String\n    public var format: String\n    public var createdAt: Date\n    public var latestVersionId: Version.ID\n    public var versionCount: Int\n    public var chunkCount: Int\n    public var totalSize: Int64\n\n    public init(snapshotId: String = UUID().uuidString, format: String, createdAt: Date = Date(), latestVersionId: Version.ID, versionCount: Int, chunkCount: Int, totalSize: Int64) {\n        self.snapshotId = snapshotId\n        self.format = format\n        self.createdAt = createdAt\n        self.latestVersionId = latestVersionId\n        self.versionCount = versionCount\n        self.chunkCount = chunkCount\n        self.totalSize = totalSize\n    }\n}\n\n/// Policy controlling automatic snapshot creation after sync.\npublic struct SnapshotPolicy {\n    public var enabled: Bool\n    public var minimumInterval: TimeInterval\n    public var minimumNewVersions: Int\n\n    public init(enabled: Bool, minimumInterval: TimeInterval, minimumNewVersions: Int) {\n        self.enabled = enabled\n        self.minimumInterval = minimumInterval\n        self.minimumNewVersions = minimumNewVersions\n    }\n\n    public static let auto = SnapshotPolicy(\n        enabled: true, minimumInterval: 7*24*3600, minimumNewVersions: 20\n    )\n    public static let disabled = SnapshotPolicy(\n        enabled: false, minimumInterval: 0, minimumNewVersions: 0\n    )\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/SnapshotCapable+ZIP.swift",
    "content": "//\n//  SnapshotCapable+ZIP.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport ZIPFoundation\n\nextension SnapshotCapable {\n\n    public var snapshotFormat: String { \"zip-v1\" }\n\n    public func writeSnapshotChunks(storeRootURL: URL, to directory: URL, maxChunkSize: Int) throws -> SnapshotManifest {\n        let fm = FileManager()\n        try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)\n\n        // Create zip of the entire store directory\n        let zipURL = directory.appendingPathComponent(\"snapshot.zip\")\n        try fm.zipItem(at: storeRootURL, to: zipURL, shouldKeepParent: false, compressionMethod: .deflate)\n        defer { try? fm.removeItem(at: zipURL) }\n\n        // Get total compressed size\n        let zipAttributes = try fm.attributesOfItem(atPath: zipURL.path)\n        let totalSize = (zipAttributes[.size] as? Int64) ?? 0\n\n        // Split zip into chunks using FileHandle for streaming\n        let readHandle = try FileHandle(forReadingFrom: zipURL)\n        defer { try? readHandle.close() }\n\n        var chunkIndex = 0\n        var bytesRemaining = totalSize\n        while bytesRemaining > 0 {\n            let bytesToRead = min(Int(bytesRemaining), maxChunkSize)\n            let chunkData = readHandle.readData(ofLength: bytesToRead)\n            if chunkData.isEmpty { break }\n\n            let chunkFile = directory.appendingPathComponent(String(format: \"chunk-%03d\", chunkIndex))\n            try chunkData.write(to: chunkFile)\n            chunkIndex += 1\n            bytesRemaining -= Int64(chunkData.count)\n        }\n\n        // Scan versions/ for manifest metadata\n        let versionsDir = storeRootURL.appendingPathComponent(\"versions\")\n        var versionCount = 0\n        var latestVersionId = Version.ID(\"\")\n        var maxTimestamp: TimeInterval = 0\n        if let versionsEnum = fm.enumerator(at: versionsDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) {\n            for case let fileURL as URL in versionsEnum {\n                guard fileURL.pathExtension == \"json\" else { continue }\n                var isDir: ObjCBool = false\n                guard fm.fileExists(atPath: fileURL.path, isDirectory: &isDir), !isDir.boolValue else { continue }\n                versionCount += 1\n                if let data = try? Data(contentsOf: fileURL),\n                   let version = try? JSONDecoder().decode(Version.self, from: data),\n                   version.timestamp > maxTimestamp {\n                    maxTimestamp = version.timestamp\n                    latestVersionId = version.id\n                }\n            }\n        }\n\n        return SnapshotManifest(\n            format: snapshotFormat,\n            latestVersionId: latestVersionId,\n            versionCount: versionCount,\n            chunkCount: chunkIndex,\n            totalSize: totalSize\n        )\n    }\n\n    public func restoreFromSnapshotChunks(storeRootURL: URL, from directory: URL, manifest: SnapshotManifest) throws {\n        let fm = FileManager()\n\n        // Concatenate chunks into a zip file\n        let zipURL = directory.appendingPathComponent(\"snapshot.zip\")\n        fm.createFile(atPath: zipURL.path, contents: nil)\n        let writeHandle = try FileHandle(forWritingTo: zipURL)\n\n        for i in 0..<manifest.chunkCount {\n            let chunkFile = directory.appendingPathComponent(String(format: \"chunk-%03d\", i))\n            let chunkData = try Data(contentsOf: chunkFile)\n            writeHandle.write(chunkData)\n        }\n        try writeHandle.close()\n\n        // Unzip to store root\n        try fm.unzipItem(at: zipURL, to: storeRootURL)\n        try? fm.removeItem(at: zipURL)\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Storage.swift",
    "content": "//\n//  Storage.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\n\npublic enum MapType {\n    case valuesByVersion // Main map for identifying which values are in each version\n    case userDefined(label: String)\n}\n\npublic protocol Storage {\n\n    func makeValuesZone(in store: Store) throws -> Zone\n    func makeMapZone(for type: MapType, in store: Store) throws -> Zone\n\n}\n\n/// Storage backends that can produce and consume chunked snapshots.\npublic protocol SnapshotCapable {\n    var snapshotFormat: String { get }\n\n    func writeSnapshotChunks(\n        storeRootURL: URL, to directory: URL, maxChunkSize: Int\n    ) throws -> SnapshotManifest\n\n    func restoreFromSnapshotChunks(\n        storeRootURL: URL, from directory: URL, manifest: SnapshotManifest\n    ) throws\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Store.swift",
    "content": "//\n//  Store.swift\n//  llvs\n//\n//  Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\nimport Synchronization\n\n\n// MARK:- Branch\n\npublic struct Branch: RawRepresentable {\n    public let rawValue: String\n    \n    public init(rawValue: String) {\n        self.rawValue = rawValue\n    }\n    \n    public init(randomizedNameBasedOn base: String = \"\") {\n        let separator = base.isEmpty ? \"\" : \"_\"\n        self.rawValue = \"\\(base)\\(separator)\\(UUID().uuidString)\"\n    }\n}\n\n\n// MARK:- Store\n\npublic final class Store {\n    \n    public enum Error: Swift.Error {\n        case missingVersion\n        case attemptToLocateUnversionedValue\n        case attemptToStoreValueWithNoVersion\n        case noCommonAncestor(firstVersion: Version.ID, secondVersion: Version.ID)\n        case unresolvedConflict(valueId: Value.ID, valueFork: Value.Fork)\n        case attemptToAddExistingVersion(Version.ID)\n        case attemptToAddVersionWithNonexistingPredecessors(Version)\n    }\n    \n    public let rootDirectoryURL: URL\n    public let valuesDirectoryURL: URL\n    public let versionsDirectoryURL: URL\n    public let mapsDirectoryURL: URL\n    public let valuesMapDirectoryURL: URL\n    \n    public let storage: Storage\n\n    private lazy var valuesZone: Zone = {\n        return try! storage.makeValuesZone(in: self)\n    }()\n    \n    private let valuesMapName = \"__llvs_values\"\n    private lazy var valuesMap: Map = {\n        let valuesMapZone = try! self.storage.makeMapZone(for: .valuesByVersion, in: self)\n        return Map(zone: valuesMapZone)\n    }()\n    \n    private let history = Mutex(History())\n\n    fileprivate let fileManager = FileManager()\n    \n    public init(rootDirectoryURL: URL, storage: Storage = FileStorage()) throws {\n        self.storage = storage\n        \n        self.rootDirectoryURL = rootDirectoryURL.resolvingSymlinksInPath()\n        self.valuesDirectoryURL = rootDirectoryURL.appendingPathComponent(\"values\")\n        self.versionsDirectoryURL = rootDirectoryURL.appendingPathComponent(\"versions\")\n        self.mapsDirectoryURL = rootDirectoryURL.appendingPathComponent(\"maps\")\n        self.valuesMapDirectoryURL = self.mapsDirectoryURL.appendingPathComponent(valuesMapName)\n\n        try? fileManager.createDirectory(at: self.rootDirectoryURL, withIntermediateDirectories: true, attributes: nil)\n        try? fileManager.createDirectory(at: self.valuesDirectoryURL, withIntermediateDirectories: true, attributes: nil)\n        try? fileManager.createDirectory(at: self.versionsDirectoryURL, withIntermediateDirectories: true, attributes: nil)\n        try? fileManager.createDirectory(at: self.mapsDirectoryURL, withIntermediateDirectories: true, attributes: nil)\n        \n        try reloadHistory()\n    }\n\n    /// Call this to make sure all history is loaded. For example, if the store could have been\n    /// changed by another process, calling this method will ensure the versions added by that process\n    /// are loaded.\n    public func reloadHistory() throws {\n        try history.withLock { history in\n            var newVersions: Set<Version> = []\n            for version in try storedVersions() where history.version(identifiedBy: version.id) == nil {\n                newVersions.insert(version)\n                try history.add(version, updatingPredecessorVersions: false)\n            }\n            for version in newVersions {\n                try history.updateSuccessors(inPredecessorsOf: version)\n            }\n        }\n    }\n    \n    /// Provides access to the history object in a serialized way, allowing access from any thread.\n    /// Calls the block passed after getting exclusive history to the history object, and passes the history.\n    public func queryHistory(in block: (History) throws ->Void) rethrows {\n        try history.withLock { history in\n            try block(history)\n        }\n    }\n    \n    public func historyIncludesVersions(identifiedBy versionIds: [Version.ID]) -> Bool {\n        var valid = false\n        queryHistory { history in\n            valid = versionIds.allSatisfy { history.version(identifiedBy: $0) != nil }\n        }\n        return valid\n    }\n    \n}\n\n\n// MARK:- Storing Values and Versions\n\nextension Store {\n\n    /// Convenience to avoid having to create Value.Change values yourself\n    @discardableResult public func makeVersion(basedOnPredecessor versionId: Version.ID?, inserting insertedValues: [Value] = [], updating updatedValues: [Value] = [], removing removedIds: [Value.ID] = [], metadata: Version.Metadata = [:]) throws -> Version {\n        let predecessors = versionId.flatMap { Version.Predecessors(idOfFirst: $0, idOfSecond: nil) }\n        let inserts: [Value.Change] = insertedValues.map { .insert($0) }\n        let updates: [Value.Change] = updatedValues.map { .update($0) }\n        let removes: [Value.Change] = removedIds.map { .remove($0) }\n        return try makeVersion(basedOn: predecessors, storing: inserts+updates+removes, metadata: metadata)\n    }\n    \n    @discardableResult public func makeVersion(basedOnPredecessor version: Version.ID?, storing changes: [Value.Change], metadata: Version.Metadata = [:]) throws -> Version {\n        let predecessors = version.flatMap { Version.Predecessors(idOfFirst: $0, idOfSecond: nil) }\n        return try makeVersion(basedOn: predecessors, storing: changes, metadata: metadata)\n    }\n    \n    /// Changes must include all updates to the map of the first predecessor. If necessary, preserves should be included to bring values\n    /// from the second predecessor into the first predecessor map.\n    @discardableResult internal func makeVersion(basedOn predecessors: Version.Predecessors?, storing changes: [Value.Change], metadata: Version.Metadata = [:]) throws -> Version {\n        let version = Version(predecessors: predecessors, valueDataSize: changes.valueDataSize, metadata: metadata)\n        try addVersion(version, storing: changes)\n        return version\n    }\n    \n    /// This method does not check consistency, and does not automatically update the map.\n    /// It is assumed that any changes to the first predecessor that are needed in the map\n    /// are present as preserves from the second predecessor.\n    internal func addVersion(_ version: Version, storing changes: [Value.Change]) throws {\n        guard !historyIncludesVersions(identifiedBy: [version.id]) else {\n            throw Error.attemptToAddExistingVersion(version.id)\n        }\n        guard historyIncludesVersions(identifiedBy: version.predecessors?.ids ?? []) else {\n            throw Error.attemptToAddVersionWithNonexistingPredecessors(version)\n        }\n        \n        // Store values\n        var valueDataSize: Int64 = 0\n        for change in changes {\n            switch change {\n            case .insert(let value), .update(let value):\n                var newValue = value\n                newValue.storedVersionId = version.id\n                try self.store(newValue)\n                valueDataSize += Int64(newValue.data.count)\n            case .remove, .preserve, .preserveRemoval:\n                continue\n            }\n        }\n        \n        // Update values map\n        let deltas: [Map.Delta] = changes.map { change in\n            switch change {\n            case .insert(let value), .update(let value):\n                let valueRef = Value.Reference(valueId: value.id, storedVersionId: version.id)\n                var delta = Map.Delta(key: Map.Key(value.id.rawValue))\n                delta.addedValueReferences = [valueRef]\n                return delta\n            case .remove(let valueId), .preserveRemoval(let valueId):\n                var delta = Map.Delta(key: Map.Key(valueId.rawValue))\n                delta.removedValueIdentifiers = [valueId]\n                return delta\n            case .preserve(let valueRef):\n                var delta = Map.Delta(key: Map.Key(valueRef.valueId.rawValue))\n                delta.addedValueReferences = [valueRef]\n                return delta\n            }\n        }\n        try valuesMap.addVersion(version.id, basedOn: version.predecessors?.idOfFirst, applying: deltas)\n        \n        // Store version\n        var versionWithDataSize = version\n        versionWithDataSize.valueDataSize = valueDataSize\n        try store(versionWithDataSize)\n        \n        // Add to history\n        try history.withLock { history in\n            do {\n                try history.add(version, updatingPredecessorVersions: true)\n            } catch History.Error.attemptToAddPreexistingVersion {\n                throw Error.attemptToAddExistingVersion(version.id)\n            }\n        }\n    }\n    \n    private func store(_ value: Value) throws {\n        guard let zoneRef = value.zoneReference else { throw Error.attemptToStoreValueWithNoVersion }\n        try valuesZone.store(value.data, for: zoneRef)\n    }\n}\n\n\n// MARK:- Fetching Values\n\nextension Store {\n    \n    public func valueReference(id valueId: Value.ID, at versionId: Version.ID) throws -> Value.Reference? {\n        return try valuesMap.valueReferences(matching: .init(valueId.rawValue), at: versionId).first\n    }\n    \n    public func valueReferences(at version: Version.ID) throws -> [Value.Reference] {\n        var refs: [Value.Reference] = []\n        try enumerate(version: version) { ref in\n            refs.append(ref)\n        }\n        return refs\n    }\n    \n    /// Convenient method to avoid having to create id types\n    public func value(idString valueIdString: String, at versionId: Version.ID) throws -> Value? {\n        return try value(id: .init(valueIdString), at: versionId)\n    }\n    \n    public func value(id valueId: Value.ID, at versionId: Version.ID) throws -> Value? {\n        let ref = try valueReference(id: valueId, at: versionId)\n        return try ref.flatMap { try value(id: valueId, storedAt: $0.storedVersionId) }\n    }\n    \n    public func value(id valueId: Value.ID, storedAt versionId: Version.ID) throws -> Value? {\n        guard let data = try valuesZone.data(for: .init(key: valueId.rawValue, version: versionId)) else { return nil }\n        let value = Value(id: valueId, storedVersionId: versionId, data: data)\n        return value\n    }\n    \n    public func value(storedAt valueReference: Value.Reference) throws -> Value? {\n        return try value(id: valueReference.valueId, storedAt: valueReference.storedVersionId)\n    }\n    \n    public func enumerate(version versionId: Version.ID, executingForEach block: (Value.Reference) throws -> Void) throws {\n        try valuesMap.enumerateValueReferences(forVersionIdentifiedBy: versionId, executingForEach: block)\n    }\n    \n}\n\n\n// MARK:- Merging\n\nextension Store {\n    \n    public enum MergeHeadSelection {\n        case all\n        case allExceptBranches\n        case allUnbranchedAndSpecificBranches([Branch])\n        case specificBranchesOnly([Branch])\n    }\n    \n    /// Whether there is more than one head\n    public var hasMultipleHeads: Bool {\n        var result: Bool = false\n        queryHistory { history in\n            result = history.headIdentifiers.count > 1\n        }\n        return result\n    }\n    \n    /// Finds any heads that have the branch in the metadata.\n    public func heads(withBranch branch: Branch) -> [Version.ID] {\n        var result: [Version.ID] = []\n        queryHistory { history in\n            let heads = history.headIdentifiers\n            result = heads.filter { id in\n                let version = history.version(identifiedBy: id)!\n                return branch.rawValue == version.metadata[.branch]?.value()\n            }\n        }\n        return result\n    }\n    \n    /// Merges heads into the version passed, which is usually a head itself. This is a convenience\n    /// to save looping through all heads.\n    /// If the version ends up being changed by the merging, the new version is returned, otherwise nil.\n    public func mergeHeads(into version: Version.ID, resolvingWith arbiter: MergeArbiter, headSelection: MergeHeadSelection = .allExceptBranches, metadata: Version.Metadata = [:]) -> Version.ID? {\n        var heads: Set<Version.ID> = []\n        var versionsById: [Version.ID:Version] = [:]\n        queryHistory { history in\n            heads = history.headIdentifiers\n            versionsById = .init(uniqueKeysWithValues: heads.map({ ($0, history.version(identifiedBy: $0)!) }))\n        }\n        heads.remove(version)\n        \n        heads = heads.filter { id in\n            let version = versionsById[id]!\n            let branch: String? = version.metadata[.branch]?.value()\n            switch headSelection {\n            case .all:\n                return true\n            case .allExceptBranches:\n                return branch == nil\n            case .allUnbranchedAndSpecificBranches(let branches):\n                return branch == nil || branches.map({ $0.rawValue }).contains(branch)\n            case .specificBranchesOnly(let branches):\n                return branches.map({ $0.rawValue }).contains(branch)\n            }\n        }\n        \n        guard !heads.isEmpty else { return nil }\n        \n        var versionId: Version.ID = version\n        for otherHead in heads {\n            let newVersion = try! merge(version: versionId, with: otherHead, resolvingWith: arbiter, metadata: metadata)\n            versionId = newVersion.id\n        }\n        \n        return versionId\n    }\n    \n    /// Will choose between a three way merge, and a two way merge, based on whether a common ancestor is found.\n    public func merge(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {\n        do {\n            return try mergeRelated(version: firstVersionIdentifier, with: secondVersionIdentifier, resolvingWith: arbiter, metadata: metadata)\n        } catch Error.noCommonAncestor {\n            return try mergeUnrelated(version: firstVersionIdentifier, with: secondVersionIdentifier, resolvingWith: arbiter, metadata: metadata)\n        }\n    }\n    \n    /// Two-way merge between two versions that have no common ancestry. Effectively we assume an empty common ancestor,\n    /// so that all changes are inserts, or conflicting twiceInserts.\n    public func mergeUnrelated(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {\n        var firstVersion, secondVersion: Version?\n        var fastForwardVersion: Version?\n        try history.withLock { history in\n            firstVersion = history.version(identifiedBy: firstVersionIdentifier)\n            secondVersion = history.version(identifiedBy: secondVersionIdentifier)\n\n            guard firstVersion != nil, secondVersion != nil else {\n                throw Error.missingVersion\n            }\n\n            // Check for fast forward\n            if history.isAncestralLine(from: firstVersion!.id, to: secondVersion!.id) {\n                fastForwardVersion = secondVersion\n            } else if history.isAncestralLine(from: secondVersion!.id, to: firstVersion!.id) {\n                fastForwardVersion = firstVersion\n            }\n        }\n        \n        if let fastForwardVersion = fastForwardVersion {\n            return fastForwardVersion\n        }\n\n        return try merge(firstVersion!, and: secondVersion!, withCommonAncestor: nil, resolvingWith: arbiter, metadata: metadata)\n    }\n    \n    /// Three-way merge between two versions, and a common ancestor. If no common ancestor is found, a .noCommonAncestor error is thrown.\n    /// Conflicts are resolved using the MergeArbiter passed in.\n    public func mergeRelated(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {\n        var firstVersion, secondVersion, commonVersion: Version?\n        var commonVersionIdentifier: Version.ID?\n        try history.withLock { history in\n            commonVersionIdentifier = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: (firstVersionIdentifier, secondVersionIdentifier))\n            guard commonVersionIdentifier != nil else {\n                throw Error.noCommonAncestor(firstVersion: firstVersionIdentifier, secondVersion: secondVersionIdentifier)\n            }\n\n            firstVersion = history.version(identifiedBy: firstVersionIdentifier)\n            secondVersion = history.version(identifiedBy: secondVersionIdentifier)\n            commonVersion = history.version(identifiedBy: commonVersionIdentifier!)\n\n            guard firstVersion != nil, secondVersion != nil else {\n                throw Error.missingVersion\n            }\n        }\n        \n        // Check for fast forward cases where no merge is needed\n        if firstVersionIdentifier == commonVersionIdentifier {\n            return secondVersion!\n        } else if secondVersionIdentifier == commonVersionIdentifier {\n            return firstVersion!\n        }\n        \n        return try merge(firstVersion!, and: secondVersion!, withCommonAncestor: commonVersion!, resolvingWith: arbiter, metadata: metadata)\n    }\n    \n    /// Two or three-way merge. Does no check to see if fast forwarding is possible. Will carry out the merge regardless of history.\n    /// If a common ancestor is supplied, it is 3-way, and otherwise 2-way.\n    private func merge(_ firstVersion: Version, and secondVersion: Version, withCommonAncestor commonAncestor: Version?, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {\n        // Prepare merge\n        let predecessors = Version.Predecessors(idOfFirst: firstVersion.id, idOfSecond: secondVersion.id)\n        let diffs = try valuesMap.differences(between: firstVersion.id, and: secondVersion.id, withCommonAncestor: commonAncestor?.id)\n        var merge = Merge(versions: (firstVersion, secondVersion), commonAncestor: commonAncestor)\n        let forkTuples = diffs.map({ ($0.valueId, $0.valueFork) })\n        merge.forksByValueIdentifier = .init(uniqueKeysWithValues: forkTuples)\n        \n        // Resolve with arbiter\n        var changes = try arbiter.changes(toResolve: merge, in: self)\n        \n        // Check changes resolve conflicts\n        let idsInChanges = Set(changes.valueIds)\n        for diff in diffs {\n            if diff.valueFork.isConflicting && !idsInChanges.contains(diff.valueId) {\n                throw Error.unresolvedConflict(valueId: diff.valueId, valueFork: diff.valueFork)\n            }\n        }\n        \n        // Must make sure any change that was made in the second predecessor is included,\n        // via a 'preserve' if necessary.\n        // This is so the map of the first predecessor is updated properly.\n        for diff in diffs where !idsInChanges.contains(diff.valueId) {\n            switch diff.valueFork {\n            case .inserted(let branch) where branch == .second:\n                fallthrough\n            case .updated(let branch) where branch == .second:\n                let ref = try valueReference(id: diff.valueId, at: secondVersion.id)!\n                changes.append(.preserve(ref))\n            case .removed(let branch) where branch == .second:\n                changes.append(.preserveRemoval(diff.valueId))\n            default:\n                break\n            }\n        }\n        \n        return try makeVersion(basedOn: predecessors, storing: changes, metadata: metadata)\n    }\n    \n}\n\n\n// MARK:- Value Changes\n\nextension Store {\n    \n    /// Returns the changes actually made in the version passed. This is important for an exchange, for example, that wishes to\n    /// store a set of changes. Note that it is not exactly equivalent to taking the diff between the  version and one of its predecessors,\n    /// because in that case, any changes made in the branch of the other predecessor will also be included as changes, when they don't\n    /// really belong (ie they were actually made in the past)\n    public func valueChanges(madeInVersionIdentifiedBy versionId: Version.ID) throws -> [Value.Change] {\n        guard let version = try version(identifiedBy: versionId) else { throw Error.missingVersion }\n        \n        guard let predecessors = version.predecessors else {\n            var changes: [Value.Change] = []\n            try valuesMap.enumerateValueReferences(forVersionIdentifiedBy: versionId) { ref in\n                let v = try value(id: ref.valueId, storedAt: ref.storedVersionId)!\n                changes.append(.insert(v))\n            }\n            return changes\n        }\n        \n        var changes: [Value.Change] = []\n        let p1 = predecessors.idOfFirst\n        if let p2 = predecessors.idOfSecond {\n            // Do a reverse-in-time fork, and negate the outcome\n            let diffs = try valuesMap.differences(between: p1, and: p2, withCommonAncestor: versionId)\n            for diff in diffs {\n                switch diff.valueFork {\n                case .twiceInserted:\n                    changes.append(.remove(diff.valueId))\n                case .twiceUpdated, .removedAndUpdated:\n                    let value = try self.value(id: diff.valueId, at: versionId)!\n                    changes.append(.update(value))\n                case .twiceRemoved:\n                    let value = try self.value(id: diff.valueId, at: versionId)!\n                    changes.append(.insert(value))\n                case .inserted:\n                    changes.append(.preserveRemoval(diff.valueId))\n                case .removed, .updated:\n                    let value = try self.value(id: diff.valueId, at: versionId)!\n                    changes.append(.preserve(value.reference!))\n                }\n            }\n        } else {\n            changes = try valueChanges(madeBetween: p1, and: version.id)\n        }\n        \n        return changes\n    }\n    \n    /// Changes that can be applied to go from the first version to the second. Useful for \"diffing\", eg, updating UI by seeing what changed.\n    public func valueChanges(madeBetween versionId1: Version.ID, and versionId2: Version.ID) throws -> [Value.Change] {\n        guard let _ = try version(identifiedBy: versionId1), let _ = try version(identifiedBy: versionId2) else { throw Error.missingVersion }\n        \n        var changes: [Value.Change] = []\n        let diffs = try valuesMap.differences(between: versionId2, and: versionId1, withCommonAncestor: versionId1)\n        for diff in diffs {\n            switch diff.valueFork {\n            case .inserted:\n                let value = try self.value(id: diff.valueId, at: versionId2)!\n                changes.append(.insert(value))\n            case .removed:\n                changes.append(.remove(diff.valueId))\n            case .updated:\n                let value = try self.value(id: diff.valueId, at: versionId2)!\n                changes.append(.update(value))\n            case .removedAndUpdated, .twiceInserted, .twiceRemoved, .twiceUpdated:\n                fatalError(\"Should not be possible with only a single branch\")\n            }\n        }\n        \n        return changes\n    }\n    \n}\n\n\n// MARK:- Storing and Fetching Versions\n\nextension Store {\n    \n    fileprivate func store(_ version: Version) throws {\n        try autoreleasepool {\n            let (dir, file) = fileSystemLocation(forVersionIdentifiedBy: version.id)\n            try? fileManager.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)\n            let data = try JSONEncoder().encode(version)\n            try data.write(to: file)\n        }\n    }\n    \n    /// Returns all versions of a value with the given identifier in the history.\n    /// Order is topological, from recent to ancient. No timestamp ordering has been applied\n    /// This can be expensive, as it iterates all history.\n    public func versionIds(for valueId: Value.ID) throws -> [Version.ID] {\n        var existingVersions: Set<Version.ID> = []\n        var valueVersions: [Version.ID] = []\n        try queryHistory { history in\n            for v in history {\n                if let ref = try valueReference(id: valueId, at: v.id), !existingVersions.contains(ref.storedVersionId) {\n                    valueVersions.append(ref.storedVersionId)\n                    existingVersions.insert(ref.storedVersionId)\n                }\n            }\n        }\n        return valueVersions\n    }\n    \n    \n    /// Version ids found in store. This makes no use of the loaded history.\n    internal func storedVersionIds(for valueId: Value.ID) throws -> [Version.ID] {\n        let valueDirectoryURL = valuesDirectoryURL.appendingSplitPathComponent(valueId.rawValue)\n        let enumerator = fileManager.enumerator(at: valueDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!\n        let valueDirComponents = valueDirectoryURL.standardizedFileURL.pathComponents\n        var versionIds: [Version.ID] = []\n        for any in enumerator {\n            var isDirectory: ObjCBool = true\n            guard let url = any as? URL else { continue }\n            guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }\n            guard url.pathExtension == \"json\" else { continue }\n            let allComponents = url.standardizedFileURL.deletingPathExtension().pathComponents\n            let versionComponents = allComponents[valueDirComponents.count...]\n            let versionString = versionComponents.joined()\n            versionIds.append(.init(versionString))\n        }\n        return versionIds\n    }\n    \n    /// Versions found in store. This makes no use of the loaded history.\n    fileprivate func storedVersions() throws -> [Version] {\n        try autoreleasepool {\n            let enumerator = fileManager.enumerator(at: versionsDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!\n            var versions: [Version] = []\n            let decoder = JSONDecoder()\n            for any in enumerator {\n                var isDirectory: ObjCBool = true\n                guard let url = any as? URL else { continue }\n                guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }\n                guard url.pathExtension == \"json\" else { continue }\n                let data = try Data(contentsOf: url)\n                let version = try decoder.decode(Version.self, from: data)\n                versions.append(version)\n            }\n            return versions\n        }\n    }\n    \n    public func version(identifiedBy versionId: Version.ID) throws -> Version? {\n        var version: Version?\n        queryHistory { history in\n            version = history.version(identifiedBy: versionId)\n        }\n        return version\n    }\n    \n    public var mostRecentHead: Version? {\n        var version: Version?\n        queryHistory { history in\n            version = history.mostRecentHead\n        }\n        return version\n    }\n\n}\n\n\n// MARK:- File System Locations\n\nfileprivate extension Store {\n    \n    func fileSystemLocation(forVersionIdentifiedBy identifier: Version.ID) -> (directoryURL: URL, fileURL: URL) {\n        let fileURL = versionsDirectoryURL.appendingSplitPathComponent(identifier.rawValue).appendingPathExtension(\"json\")\n        let directoryURL = fileURL.deletingLastPathComponent()\n        return (directoryURL: directoryURL, fileURL: fileURL)\n    }\n}\n\n\n// MARK:- Path Utilities\n\ninternal extension URL {\n    \n    /// Appends a path to the messaged URL that consists of a filename for which\n    /// a prefix is taken as a subdirectory. Eg. `file:///root` might become\n    /// `file:///root/fi/lename.jpg` when appending `filename.jpg` with `subDirectoryNameLength` of 2.\n    func appendingSplitPathComponent(_ name: String, prefixLength: UInt = 2) -> URL {\n        guard name.count > prefixLength else {\n            return appendingPathComponent(name)\n        }\n        \n        // Embed a subdirectory\n        let index = name.index(name.startIndex, offsetBy: Int(prefixLength))\n        let prefix = String(name[..<index])\n        let postfix = String(name[index...])\n        let directory = appendingPathComponent(prefix).appendingPathComponent(postfix)\n        \n        return directory\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVS/Core/StoreCoordinator.swift",
    "content": "//\n//  StoreCoordinator.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 09/06/2019.\n//  Copyright © 2019 Momenta B.V. All rights reserved.\n//\n\nimport Foundation\nimport Synchronization\n\n/// A `StoreCoordinator` takes care of all aspects of setting up a syncing store.\n/// It's the simplest way to get started, though you may want more control for advanced use cases.\n///\n/// Thread-safety: `currentVersion` is protected by a `Mutex`. Other mutable properties\n/// (`exchange`, `mergeArbiter`, `defaultMetadataForNewVersions`, `isExchanging`) are\n/// set during initialization or within the serialized exchange flow.\npublic class StoreCoordinator: @unchecked Sendable {\n\n    private struct CachedData: Codable {\n        var exchangeRestorationData: Data?\n        var currentVersionIdentifier: Version.ID\n    }\n\n    public let store: Store\n    public let snapshotPolicy: SnapshotPolicy\n    public var exchange: (any Exchange)? {\n        didSet {\n            exchange?.restorationState = cachedData?.exchangeRestorationData\n        }\n    }\n    public var mergeArbiter: MergeArbiter = MostRecentChangeFavoringArbiter()\n\n    public var defaultMetadataForNewVersions: Version.Metadata = [:]\n\n    public let storeDirectoryURL: URL\n    public let cacheDirectoryURL: URL\n\n    private var cachedCoordinatorFileURL: URL\n\n    public var exchangeRestorationData: Data? {\n        return exchange?.restorationState\n    }\n\n    public private(set) var currentVersionUpdates: AsyncStream<Version.ID>\n    private var currentVersionContinuation: AsyncStream<Version.ID>.Continuation?\n\n    private let _currentVersion: Mutex<Version.ID>\n\n    public var currentVersion: Version.ID {\n        _currentVersion.withLock { $0 }\n    }\n\n    private func updateCurrentVersion(_ newValue: Version.ID) {\n        let changed = _currentVersion.withLock { current -> Bool in\n            guard current != newValue else { return false }\n            current = newValue\n            return true\n        }\n        if changed {\n            persist()\n            currentVersionContinuation?.yield(newValue)\n        }\n    }\n\n    private class var defaultStoreDirectory: URL {\n        let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!\n        let rootDir = appSupport.appendingPathComponent(\"LLVS\").appendingPathComponent(\"DefaultStore\")\n        return rootDir\n    }\n\n    private class var defaultCacheDirectory: URL {\n        let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!\n        let rootDir = cachesDir.appendingPathComponent(\"LLVS\").appendingPathComponent(\"CoordinatorCache\")\n        return rootDir\n    }\n\n    /// This will setup a store in the default location (Applicaton Support). If you need more than one store,\n    /// use `init(withStoreDirectoryAt:,cacheDirectoryAt:)` instead.\n    public convenience init(snapshotPolicy: SnapshotPolicy = .disabled) throws {\n        try self.init(withStoreDirectoryAt: Self.defaultStoreDirectory, cacheDirectoryAt: Self.defaultStoreDirectory, snapshotPolicy: snapshotPolicy)\n    }\n\n    /// Gives full control over where the store is (directory location), and where cached data should be kept (directory).\n    /// The directories will be created if they do not exist.\n    public init(withStoreDirectoryAt storeURL: URL, cacheDirectoryAt coordinatorCacheURL: URL, snapshotPolicy: SnapshotPolicy = .disabled) throws {\n        self.snapshotPolicy = snapshotPolicy\n        self.storeDirectoryURL = storeURL\n        self.cacheDirectoryURL = coordinatorCacheURL\n        self.cachedCoordinatorFileURL = cacheDirectoryURL.appendingPathComponent(\"Coordinator.json\")\n\n        try FileManager.default.createDirectory(at: storeURL, withIntermediateDirectories: true, attributes: nil)\n        try FileManager.default.createDirectory(at: coordinatorCacheURL, withIntermediateDirectories: true, attributes: nil)\n\n        self.store = try Store(rootDirectoryURL: storeURL)\n        self._currentVersion = Mutex(Version.ID()) // Set a temporary version. Final is in cache\n\n        var continuation: AsyncStream<Version.ID>.Continuation?\n        self.currentVersionUpdates = AsyncStream { continuation = $0 }\n        self.currentVersionContinuation = continuation\n\n        try loadCache()\n    }\n\n    private func loadCache() throws {\n        // Load state from cache\n        let cachedData: CachedData\n        var shouldPersist = false\n        if let cached = self.cachedData {\n            cachedData = cached\n        } else {\n            // Get most recent, or make first commit\n            let version: Version.ID\n            if let head = store.mostRecentHead {\n                version = head.id\n            } else {\n                version = try store.makeVersion(basedOnPredecessor: nil, storing: []).id\n            }\n            cachedData = CachedData(currentVersionIdentifier: version)\n            shouldPersist = true\n        }\n\n        // Set properties from cache\n        updateCurrentVersion(cachedData.currentVersionIdentifier)\n        if shouldPersist { persist() }\n    }\n\n    private var cachedData: CachedData? {\n        let fileManager = FileManager()\n        if fileManager.fileExists(atPath: self.cachedCoordinatorFileURL.path),\n            let data = try? Data(contentsOf: self.cachedCoordinatorFileURL),\n            let cached = try? JSONDecoder().decode(CachedData.self, from: data) {\n            return cached\n        } else {\n            return nil\n        }\n    }\n\n    /// Store cached data\n    private func persist() {\n        let cachedData = CachedData(exchangeRestorationData: exchange?.restorationState, currentVersionIdentifier: currentVersion)\n        if let data = try? JSONEncoder().encode(cachedData) {\n            try? data.write(to: cachedCoordinatorFileURL)\n        }\n    }\n\n\n    // MARK: Heads\n\n    public func heads(withBranch branch: Branch) -> [Version.ID] {\n        store.heads(withBranch: branch)\n    }\n\n    /// Will attempt to get the first branch version, if one exists; if not, will return the current version.\n    /// This is just a convenient way to get a base version.\n    public func versionForBranchOrCurrentHead(for branch: Branch? = nil) -> Version.ID {\n        branch.flatMap({ heads(withBranch: $0).first }) ?? currentVersion\n    }\n\n\n    // MARK: Saving\n\n    /// You should use this to save instead of using the store directly, so that the\n    /// coordinator can track versions. Otherwise you will need to merge to see the changes.\n    public func save(_ changes: [Value.Change], in branch: Branch? = nil, metadata: Version.Metadata? = nil) throws {\n        guard !changes.isEmpty || metadata != nil else { return }\n        var metadata = metadata ?? defaultMetadataForNewVersions\n        if let branch = branch { metadata[.branch] = .init(branch.rawValue) }\n        updateCurrentVersion(try store.makeVersion(basedOnPredecessor: versionForBranchOrCurrentHead(for: branch), storing: changes, metadata: metadata).id)\n    }\n\n    public func save(inserting inserts: [Value] = [], updating updates: [Value] = [], removing removals: [Value.ID] = [], in branch: Branch? = nil, metadata: Version.Metadata? = nil) throws {\n        guard !inserts.isEmpty || !updates.isEmpty || !removals.isEmpty || metadata != nil else { return }\n        var metadata = metadata ?? defaultMetadataForNewVersions\n        if let branch = branch { metadata[.branch] = .init(branch.rawValue) }\n        updateCurrentVersion(try store.makeVersion(basedOnPredecessor: versionForBranchOrCurrentHead(for: branch), inserting: inserts, updating: updates, removing: removals, metadata: metadata).id)\n    }\n\n\n    // MARK: Fetching\n\n    /// Pass a specific version, or nil for the current version\n    public func valueReferences(at version: Version.ID? = nil) throws -> [Value.Reference] {\n        try store.valueReferences(at: version ?? currentVersion)\n    }\n\n    public func values(at version: Version.ID? = nil) throws -> [Value] {\n        try autoreleasepool {\n            return try valueReferences(at: version).map { try store.value(storedAt: $0)! }\n        }\n    }\n\n    public func value(idString: String) throws -> Value? {\n        return try store.value(idString: idString, at: currentVersion)\n    }\n\n    public func value(id: Value.ID) throws -> Value? {\n        return try store.value(id: id, at: currentVersion)\n    }\n\n    public func value(idString: String, on branch: Branch?) throws -> Value? {\n        return try store.value(idString: idString, at: versionForBranchOrCurrentHead(for: branch))\n    }\n\n    public func value(id: Value.ID, on branch: Branch?) throws -> Value? {\n        return try store.value(id: id, at: versionForBranchOrCurrentHead(for: branch))\n    }\n\n\n    // MARK: Sync\n\n    public private(set) var isExchanging = false\n\n    /// Serializer to ensure one exchange at a time.\n    private var exchangeSerializer = ExchangeSerializer()\n\n    /// This transfers data between cloud and local store, but does not alter the current branch or do any merging.\n    /// It's a bit like a two-way version of Git's fetch.\n    public func exchange() async throws {\n        try await exchangeSerializer.enqueue { [self] in\n            try await self.performExchange()\n        }\n    }\n\n    private func performExchange() async throws {\n        isExchanging = true\n        defer { isExchanging = false }\n\n        guard let exchange = exchange else { return }\n\n        let _ = try await exchange.retrieve()\n        let _ = try await exchange.send()\n\n        // Upload snapshot if policy allows (fire and forget)\n        Task { [weak self] in\n            try? await self?.uploadSnapshotIfNeeded()\n        }\n    }\n\n    /// Merging any extra heads, or fast forward to latest. It's a good idea to save data just before calling this, so that\n    /// in view edits are committed. Returns true if the merge changed the current version; false otherwise.\n    /// Note that the default behavior is not to merge in named branches. These are usually used for background work, and need to be merged in under controlled circumstances.\n    @discardableResult public func merge(metadata: Version.Metadata? = nil, headSelection: Store.MergeHeadSelection = .allExceptBranches) -> Bool {\n        let metadata = metadata ?? defaultMetadataForNewVersions\n        let newVersion = self.store.mergeHeads(into: self.currentVersion, resolvingWith: self.mergeArbiter, headSelection: headSelection, metadata: metadata)\n        if let newVersion = newVersion {\n            updateCurrentVersion(newVersion)\n            return true\n        } else {\n            return false\n        }\n    }\n\n\n    // MARK: Snapshots\n\n    /// Download and restore a cloud snapshot if available and compatible.\n    /// Call before the first exchange on a new device.\n    public func bootstrapFromSnapshot() async throws {\n        guard let snapshotExchange = exchange as? SnapshotExchange,\n              let snapshotStorage = store.storage as? SnapshotCapable else {\n            return\n        }\n\n        // Check store has minimal history (≤ 1 version) — skip if already populated\n        var versionCount = 0\n        store.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        guard versionCount <= 1 else { return }\n\n        guard let manifest = try await snapshotExchange.retrieveSnapshotManifest() else { return }\n\n        // Check format matches\n        guard manifest.format == snapshotStorage.snapshotFormat else { return }\n\n        // Download chunks to temp directory\n        let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)\n        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)\n        defer { try? FileManager.default.removeItem(at: tempDir) }\n\n        for i in 0..<manifest.chunkCount {\n            let data = try await snapshotExchange.retrieveSnapshotChunk(index: i)\n            let chunkFile = tempDir.appendingPathComponent(String(format: \"chunk-%03d\", i))\n            try data.write(to: chunkFile)\n        }\n\n        try snapshotStorage.restoreFromSnapshotChunks(\n            storeRootURL: self.store.rootDirectoryURL,\n            from: tempDir,\n            manifest: manifest\n        )\n        try self.store.reloadHistory()\n\n        // Update currentVersion to the latest head\n        if let head = self.store.mostRecentHead {\n            updateCurrentVersion(head.id)\n        }\n    }\n\n    internal func uploadSnapshotIfNeeded() async throws {\n        guard snapshotPolicy.enabled,\n              let snapshotExchange = exchange as? SnapshotExchange,\n              let snapshotStorage = store.storage as? SnapshotCapable else {\n            return\n        }\n\n        // Check current version count\n        var currentVersionCount = 0\n        store.queryHistory { history in\n            currentVersionCount = history.allVersionIdentifiers.count\n        }\n\n        let existingManifest = try await snapshotExchange.retrieveSnapshotManifest()\n\n        if let manifest = existingManifest {\n            // Skip if recent enough\n            if manifest.createdAt.timeIntervalSinceNow > -self.snapshotPolicy.minimumInterval {\n                return\n            }\n            // Skip if not enough new versions\n            if currentVersionCount - manifest.versionCount < self.snapshotPolicy.minimumNewVersions {\n                return\n            }\n        }\n\n        // Write snapshot to temp directory\n        let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)\n        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)\n        defer { try? FileManager.default.removeItem(at: tempDir) }\n\n        let manifest = try snapshotStorage.writeSnapshotChunks(\n            storeRootURL: self.store.rootDirectoryURL,\n            to: tempDir,\n            maxChunkSize: 5_000_000\n        )\n\n        try await snapshotExchange.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = tempDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n    }\n}\n\n\n// MARK: - ExchangeSerializer\n\n/// Ensures only one exchange runs at a time using an AsyncStream as a FIFO queue.\nprivate final class ExchangeSerializer: @unchecked Sendable {\n    private typealias Work = @Sendable () async throws -> Void\n    private let stream: AsyncStream<Work>\n    private let continuation: AsyncStream<Work>.Continuation\n\n    init() {\n        (stream, continuation) = AsyncStream<Work>.makeStream()\n        Task { [stream] in\n            for await work in stream {\n                try? await work()\n            }\n        }\n    }\n\n    func enqueue(_ work: @escaping @Sendable () async throws -> Void) async throws {\n        try await withCheckedThrowingContinuation { (done: CheckedContinuation<Void, Error>) in\n            continuation.yield {\n                do {\n                    try await work()\n                    done.resume()\n                } catch {\n                    done.resume(throwing: error)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Value.swift",
    "content": "//\n//  Value.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\n\npublic struct Value: Codable, Identifiable {\n    public typealias ID = Identifier\n    \n    public var id: ID\n    public var data: Data\n    \n    /// The identifier of the version in which this value was stored. Can be nil, if\n    /// a value has not yet been stored.\n    public internal(set) var storedVersionId: Version.ID?\n    \n    internal var zoneReference: ZoneReference? {\n        guard let version = storedVersionId else { return nil }\n        return ZoneReference(key: id.rawValue, version: version)\n    }\n    \n    public var reference: Reference? {\n        guard let version = storedVersionId else { return nil }\n        return Reference(valueId: id, storedVersionId: version)\n    }\n    \n    /// Convenience that saves creating IDs\n    public init(idString: String, data: Data) {\n        self.init(id: ID(idString), data: data)\n    }\n    \n    /// If an id is not provided, a UUID will be used. The storedVersionId will be set to nil, because\n    /// this value has not been stored yet.\n    public init(id: ID = .init(UUID().uuidString), data: Data) {\n        self.id = id\n        self.data = data\n    }\n    \n    internal init(id: ID, storedVersionId: Version.ID, data: Data) {\n        self.id = id\n        self.storedVersionId = storedVersionId\n        self.data = data\n    }\n}\n\n\npublic extension Value {\n    \n    struct Reference: Codable, Hashable {\n        public var valueId: ID\n        public var storedVersionId: Version.ID\n    }\n    \n    struct Identifier: RawRepresentable, Hashable, Codable {\n        public var rawValue: String\n        \n        public init(rawValue: String = UUID().uuidString) {\n            self.rawValue = rawValue\n        }\n        \n        public init(_ rawValue: String) {\n            self.init(rawValue: rawValue)\n        }\n    }\n    \n    enum Change: Codable {\n        case insert(Value)\n        case update(Value)\n        case remove(ID)\n        case preserve(Reference)\n        case preserveRemoval(ID)\n        \n        enum CodingKeys: String, CodingKey {\n            case insert, update, remove, preserve, preserveRemoval\n        }\n        \n        enum Error: Swift.Error {\n            case changeDecodingFailure\n        }\n        \n        public func encode(to encoder: Encoder) throws {\n            var c = encoder.container(keyedBy: CodingKeys.self)\n            switch self {\n            case let .insert(value):\n                try c.encode(value, forKey: .insert)\n            case let .update(value):\n                try c.encode(value, forKey: .update)\n            case let .remove(id):\n                try c.encode(id, forKey: .remove)\n            case let .preserve(ref):\n                try c.encode(ref, forKey: .preserve)\n            case let .preserveRemoval(id):\n                try c.encode(id, forKey: .preserveRemoval)\n            }\n        }\n        \n        public init(from decoder: Decoder) throws {\n            let c = try decoder.container(keyedBy: CodingKeys.self)\n            if let v = try? c.decode(Value.self, forKey: .insert) {\n                self = .insert(v); return\n            }\n            if let v = try? c.decode(Value.self, forKey: .update) {\n                self = .update(v); return\n            }\n            if let i = try? c.decode(ID.self, forKey: .remove) {\n                self = .remove(i); return\n            }\n            if let r = try? c.decode(Reference.self, forKey: .preserve) {\n                self = .preserve(r); return\n            }\n            if let i = try? c.decode(ID.self, forKey: .preserveRemoval) {\n                self = .preserveRemoval(i); return\n            }\n            throw Error.changeDecodingFailure\n        }\n    }\n    \n    enum Fork: Equatable {\n        public enum Branch: Equatable {\n            case first\n            case second\n            \n            var opposite: Branch {\n                return self == .first ? .second : .first\n            }\n        }\n        \n        case inserted(Branch)\n        case twiceInserted\n        case removed(Branch)\n        case twiceRemoved\n        case updated(Branch)\n        case twiceUpdated\n        case removedAndUpdated(removedOn: Branch)\n        \n        public var isConflicting: Bool {\n            switch self {\n            case .inserted, .removed, .updated, .twiceRemoved:\n                return false\n            case .twiceInserted, .twiceUpdated, .removedAndUpdated:\n                return true\n            }\n        }\n    }\n}\n\n\nextension Array where Element == Value.Change {\n    \n    var valueIds: [Value.ID] {\n        return self.map { change in\n            switch change {\n            case .insert(let value), .update(let value):\n                return value.id\n            case .remove(let identifier), .preserveRemoval(let identifier):\n                return identifier\n            case .preserve(let ref):\n                return ref.valueId\n            }\n        }\n    }\n    \n    var valueDataSize: Int64 {\n        return self.reduce(0) { result, change in\n            switch change {\n            case .insert(let value), .update(let value):\n                return result + Int64(value.data.count)\n            case .remove, .preserveRemoval, .preserve:\n                return result\n            }\n        }\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Version.swift",
    "content": "//\n//  Version.swift\n//  llvs\n//\n//  Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\n\npublic struct Version: Hashable, Identifiable {\n    \n    public typealias Metadata = [MetadataKey:MetadataValue]\n    \n    public struct MetadataKey: Hashable, Codable, RawRepresentable {\n        public var rawValue: String\n        public init(rawValue: String) { self.rawValue = rawValue }\n        \n        public static let branch = Self(rawValue: \"__llvs_branch\")\n    }\n    \n    public struct MetadataValue: Codable {\n        public let data: Data\n        public init(data: Data) { self.data = data }\n        public init<T: Codable>(_ value: T) { self.data = try! JSONEncoder().encode(value) }\n        public func value<T: Codable>() -> T { try! JSONDecoder().decode(T.self, from: self.data) }\n    }\n        \n    public typealias ID = Identifier\n\n    public var id: ID = .init()\n    public var predecessors: Predecessors?\n    public var successors: Successors = .init()\n    public var timestamp: TimeInterval\n    public var valueDataSize: Int64?\n    public var metadata: Metadata = [:]\n    \n    private enum CodingKeys: String, CodingKey {\n        case identifier\n        case predecessors\n        case timestamp\n        case valueDataSize\n        case metadata\n    }\n    \n    public init(id: ID = .init(), predecessors: Predecessors? = nil, valueDataSize: Int64, metadata: Metadata = [:]) {\n        self.id = id\n        self.predecessors = predecessors\n        self.timestamp = Date().timeIntervalSinceReferenceDate\n        self.metadata = metadata\n        self.valueDataSize = valueDataSize\n    }\n    \n    public static func == (lhs: Version, rhs: Version) -> Bool {\n        lhs.id == rhs.id\n    }\n    \n    public func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n    }\n}\n\n\nextension Version: Codable {\n    \n    public init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        id = try container.decode(ID.self, forKey: .identifier)\n        predecessors = try container.decodeIfPresent(Predecessors.self, forKey: .predecessors)\n        timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)\n        metadata = try container.decodeIfPresent(Metadata.self, forKey: .metadata) ?? [:]\n        valueDataSize = try container.decodeIfPresent(Int64.self, forKey: .valueDataSize)\n    }\n    \n    public func encode(to encoder: Encoder) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(id, forKey: .identifier)\n        try container.encodeIfPresent(predecessors, forKey: .predecessors)\n        try container.encode(timestamp, forKey: .timestamp)\n        try container.encodeIfPresent(valueDataSize, forKey: .valueDataSize)\n        try container.encode(metadata, forKey: .metadata)\n    }\n    \n}\n\n\nextension Version {\n    \n    public struct Identifier: RawRepresentable, Codable, Hashable {\n        public var rawValue: String\n        \n        public init(rawValue: String = UUID().uuidString) {\n            self.rawValue = rawValue\n        }\n        \n        public init(_ rawValue: String) {\n            self.init(rawValue: rawValue)\n        }\n    }\n    \n    public struct Predecessors: Codable, Hashable {\n        public internal(set) var idOfFirst: ID\n        public internal(set) var idOfSecond: ID?\n        public var ids: [ID] {\n            var result = [idOfFirst]\n            if let second = idOfSecond { result.append(second) }\n            return result\n        }\n        \n        internal init(idOfFirst: ID, idOfSecond: ID?) {\n            self.idOfFirst = idOfFirst\n            self.idOfSecond = idOfSecond\n        }\n    }\n    \n    public struct Successors: Codable, Hashable {\n        public internal(set) var ids: Set<ID>\n        internal init(ids: Set<ID> = []) {\n            self.ids = ids\n        }\n    }\n\n}\n\n\npublic extension Collection where Element == Version {\n    \n    var ids: [Version.ID] {\n        return map { $0.id }\n    }\n    \n    var idStrings: [String] {\n        return map { $0.id.rawValue }\n    }\n    \n}\n\n\npublic extension Collection where Element == Version.ID {\n\n    var idStrings: [String] {\n        return map { $0.rawValue }\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVS/Core/Zone.swift",
    "content": "//\n//  Zone.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 02/12/2018.\n//\n\nimport Foundation\n\npublic struct ZoneReference: Codable, Hashable {\n    public var key: String\n    public var version: Version.ID\n}\n\npublic protocol Zone {\n    func store(_ data: Data, for reference: ZoneReference) throws\n    \n    // Default provided, but zone implementations can optimize this.\n    func store(_ data: [Data], for references: [ZoneReference]) throws\n\n    func data(for reference: ZoneReference) throws -> Data?\n\n    // Default provided, but zone implementations can optimize this.\n    func data(for references: [ZoneReference]) throws -> [Data?]\n}\n\npublic extension Zone {\n\n    func store(_ data: [Data], for references: [ZoneReference]) throws {\n        try zip(data, references).forEach { data, ref in\n            try store(data, for: ref)\n        }\n    }\n\n    func data(for references: [ZoneReference]) throws -> [Data?] {\n        return try references.map { try data(for: $0) }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Exchanges/CloudFileSystem.swift",
    "content": "//\n//  CloudFileSystem.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\n\n/// Errors that can occur when interacting with a cloud file system.\npublic enum CloudFileSystemError: Error {\n    case fileNotFound\n    case uploadFailed\n    case downloadFailed\n    case directoryListingFailed\n    case authenticationFailed\n    case serverError(statusCode: Int)\n}\n\n/// A protocol for file-level operations on a cloud storage service.\n///\n/// Paths are relative strings (e.g. `\"versions/ABC-123\"`).\n/// Implementations map these to service-specific addressing.\n/// `upload` creates intermediate directories as needed.\npublic protocol CloudFileSystem: AnyObject, Sendable {\n\n    /// Returns `true` if a file exists at the given path.\n    func fileExists(at path: String) async throws -> Bool\n\n    /// Returns the file names (not full paths) of items in the directory at the given path.\n    func contentsOfDirectory(at path: String) async throws -> [String]\n\n    /// Uploads data to the given path, creating intermediate directories as needed.\n    func upload(data: Data, to path: String) async throws\n\n    /// Downloads data from the given path.\n    func download(from path: String) async throws -> Data\n\n    /// Removes the file at the given path. Does not throw if the file doesn't exist.\n    func remove(at path: String) async throws\n\n    /// Removes the directory at the given path and all its contents. Does not throw if the directory doesn't exist.\n    func removeDirectory(at path: String) async throws\n}\n"
  },
  {
    "path": "Sources/LLVS/Exchanges/CloudFileSystemExchange.swift",
    "content": "//\n//  CloudFileSystemExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\n\n/// An exchange that stores version and change data in a cloud file system.\n///\n/// Implements `Exchange` and `SnapshotExchange` by delegating all I/O to a `CloudFileSystem`.\n/// The on-disk layout mirrors `FileSystemExchange`:\n///\n/// ```\n/// {basePath}/\n///   versions/{versionId}       -- JSON: {\"version\": Version}\n///   changes/{versionId}        -- JSON: [Value.Change]\n///   snapshots/manifest.json    -- JSON: SnapshotManifest\n///   snapshots/chunk-000...     -- Binary chunk data\n/// ```\npublic final class CloudFileSystemExchange: Exchange, SnapshotExchange, @unchecked Sendable {\n\n    public enum Error: Swift.Error {\n        case versionFileInvalid\n        case changesFileInvalid\n        case snapshotChunkMissing(Int)\n    }\n\n    public let store: Store\n    public let cloudFileSystem: CloudFileSystem\n\n    /// A path prefix allowing multiple stores per cloud account.\n    public let basePath: String\n\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    public var restorationState: Data? {\n        get { return nil }\n        set {}\n    }\n\n    private var versionsPath: String { basePath + \"/versions\" }\n    private var changesPath: String { basePath + \"/changes\" }\n    private var snapshotsPath: String { basePath + \"/snapshots\" }\n\n    /// Creates a cloud file system exchange.\n    /// - Parameters:\n    ///   - cloudFileSystem: The cloud file system to use for I/O.\n    ///   - store: The local LLVS store.\n    ///   - basePath: A path prefix for all cloud files (default: empty string).\n    public init(cloudFileSystem: CloudFileSystem, store: Store, basePath: String = \"\") {\n        self.cloudFileSystem = cloudFileSystem\n        self.store = store\n        self.basePath = basePath\n        (self.newVersionsAvailable, self.newVersionsContinuation) = AsyncStream<Void>.makeStream()\n    }\n\n    deinit {\n        newVersionsContinuation.finish()\n    }\n\n    // MARK: - Exchange\n\n    public func prepareToRetrieve() async throws {\n    }\n\n    public func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        do {\n            let names = try await cloudFileSystem.contentsOfDirectory(at: versionsPath)\n            return names.map { Version.ID($0) }\n        } catch let error as CloudFileSystemError where error.isNotFound {\n            return []\n        }\n    }\n\n    public func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        try await versionIds.asyncMap { versionId in\n            let path = self.versionsPath + \"/\\(versionId.rawValue)\"\n            let data = try await self.cloudFileSystem.download(from: path)\n            if let version = try JSONDecoder().decode([String: Version].self, from: data)[\"version\"] {\n                return version\n            } else {\n                throw Error.versionFileInvalid\n            }\n        }\n    }\n\n    public func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        var result: [Version.ID: [Value.Change]] = [:]\n        for versionId in versionIds {\n            let path = changesPath + \"/\\(versionId.rawValue)\"\n            let data = try await cloudFileSystem.download(from: path)\n            let changes = try JSONDecoder().decode([Value.Change].self, from: data)\n            result[versionId] = changes\n        }\n        return result\n    }\n\n    public func prepareToSend() async throws {\n    }\n\n    public func send(versionChanges: [VersionChanges]) async throws {\n        for (version, valueChanges) in versionChanges {\n            // Upload changes before version file for consistency\n            let changesData = try JSONEncoder().encode(valueChanges)\n            try await cloudFileSystem.upload(data: changesData, to: changesPath + \"/\\(version.id.rawValue)\")\n\n            let versionData = try JSONEncoder().encode([\"version\": version])\n            try await cloudFileSystem.upload(data: versionData, to: versionsPath + \"/\\(version.id.rawValue)\")\n        }\n    }\n\n    // MARK: - Snapshot Exchange\n\n    public func retrieveSnapshotManifest() async throws -> SnapshotManifest? {\n        let manifestPath = snapshotsPath + \"/manifest.json\"\n        do {\n            let exists = try await cloudFileSystem.fileExists(at: manifestPath)\n            guard exists else { return nil }\n            let data = try await cloudFileSystem.download(from: manifestPath)\n            return try JSONDecoder().decode(SnapshotManifest.self, from: data)\n        } catch let error as CloudFileSystemError where error.isNotFound {\n            return nil\n        }\n    }\n\n    public func retrieveSnapshotChunk(index: Int) async throws -> Data {\n        let chunkPath = snapshotsPath + \"/\" + String(format: \"chunk-%03d\", index)\n        do {\n            return try await cloudFileSystem.download(from: chunkPath)\n        } catch {\n            throw Error.snapshotChunkMissing(index)\n        }\n    }\n\n    public func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws {\n        // Remove previous snapshot if any\n        try? await cloudFileSystem.removeDirectory(at: snapshotsPath)\n\n        // Write chunks first\n        for i in 0..<manifest.chunkCount {\n            let chunkData = try chunkProvider(i)\n            let chunkPath = snapshotsPath + \"/\" + String(format: \"chunk-%03d\", i)\n            try await cloudFileSystem.upload(data: chunkData, to: chunkPath)\n        }\n\n        // Write manifest last\n        let manifestData = try JSONEncoder().encode(manifest)\n        try await cloudFileSystem.upload(data: manifestData, to: snapshotsPath + \"/manifest.json\")\n    }\n}\n\n// MARK: - Async Helpers\n\nprivate extension Array {\n    func asyncMap<T>(_ transform: @escaping (Element) async throws -> T) async throws -> [T] {\n        var results: [T] = []\n        results.reserveCapacity(count)\n        for element in self {\n            try await results.append(transform(element))\n        }\n        return results\n    }\n}\n\n// MARK: - CloudFileSystemError Helpers\n\nextension CloudFileSystemError {\n    var isNotFound: Bool {\n        if case .fileNotFound = self { return true }\n        if case .directoryListingFailed = self { return true }\n        return false\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Exchanges/FileSystemExchange.swift",
    "content": "//\n//  FileSystemExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 25/02/2019.\n//\n\nimport Foundation\n\npublic class FileSystemExchange: NSObject, Exchange, NSFilePresenter, SnapshotExchange {\n\n    public enum Error: Swift.Error {\n        case versionFileInvalid\n        case changesFileInvalid\n        case snapshotChunkMissing(Int)\n    }\n\n    public let store: Store\n\n    private let minimumDelayBeforeNotifyingOfNewVersions = 1.0\n\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    public let rootDirectoryURL: URL\n    public var versionsDirectory: URL { return rootDirectoryURL.appendingPathComponent(\"versions\") }\n    public var changesDirectory: URL { return rootDirectoryURL.appendingPathComponent(\"changes\") }\n    public var snapshotsDirectory: URL { return rootDirectoryURL.appendingPathComponent(\"snapshots\") }\n\n    public let usesFileCoordination: Bool\n\n    public var restorationState: Data? {\n        get { return nil }\n        set {}\n    }\n\n    fileprivate let fileManager = FileManager()\n    fileprivate let queue = OperationQueue()\n\n    public init(rootDirectoryURL: URL, store: Store, usesFileCoordination: Bool) {\n        self.rootDirectoryURL = rootDirectoryURL\n        self.store = store\n        self.usesFileCoordination = usesFileCoordination\n        (self.newVersionsAvailable, self.newVersionsContinuation) = AsyncStream<Void>.makeStream()\n        super.init()\n        try? fileManager.createDirectory(at: rootDirectoryURL, withIntermediateDirectories: true, attributes: nil)\n        try? fileManager.createDirectory(at: versionsDirectory, withIntermediateDirectories: true, attributes: nil)\n        try? fileManager.createDirectory(at: changesDirectory, withIntermediateDirectories: true, attributes: nil)\n        if self.usesFileCoordination {\n            NSFileCoordinator.addFilePresenter(self)\n        }\n    }\n\n    deinit {\n        if self.usesFileCoordination {\n            NSFileCoordinator.removeFilePresenter(self)\n        }\n        newVersionsContinuation.finish()\n    }\n\n    public func prepareToRetrieve() async throws {\n    }\n\n    public func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        try await coordinateFileAccess(.read) {\n            let contents = try self.fileManager.contentsOfDirectory(at: self.versionsDirectory, includingPropertiesForKeys: nil, options: [])\n            return contents.map({ Version.ID($0.lastPathComponent) })\n        }\n    }\n\n    public func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        try await coordinateFileAccess(.read) {\n            try versionIds.map { versionId in\n                let url = self.versionsDirectory.appendingPathComponent(versionId.rawValue)\n                let data = try Data(contentsOf: url)\n                if let version = try JSONDecoder().decode([String:Version].self, from: data)[\"version\"] {\n                    return version\n                } else {\n                    throw Error.versionFileInvalid\n                }\n            }\n        }\n    }\n\n    public func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        try await coordinateFileAccess(.read) {\n            try versionIds.reduce(into: [:]) { result, versionId in\n                let url = self.changesDirectory.appendingPathComponent(versionId.rawValue)\n                let data = try Data(contentsOf: url)\n                let changes = try JSONDecoder().decode([Value.Change].self, from: data)\n                result[versionId] = changes\n            }\n        }\n    }\n\n    public func prepareToSend() async throws {\n    }\n\n    public func send(versionChanges: [VersionChanges]) async throws {\n        try await coordinateFileAccess(.write) {\n            for (version, valueChanges) in versionChanges {\n                let changesURL = self.changesDirectory.appendingPathComponent(version.id.rawValue)\n                let changesData = try JSONEncoder().encode(valueChanges)\n                try changesData.write(to: changesURL)\n\n                let versionURL = self.versionsDirectory.appendingPathComponent(version.id.rawValue)\n                let versionData = try JSONEncoder().encode([\"version\":version])\n                try versionData.write(to: versionURL)\n            }\n        }\n    }\n\n    private enum FileAccess {\n        case read, write\n    }\n\n    private func coordinateFileAccess<T>(_ access: FileAccess, by block: @escaping () throws -> T) async throws -> T {\n        try await withCheckedThrowingContinuation { continuation in\n            queue.addOperation {\n                if self.usesFileCoordination {\n                    let coordinator = NSFileCoordinator(filePresenter: self)\n                    var coordError: NSError?\n\n                    let accessor: (URL) -> Void = { _ in\n                        do {\n                            let result = try block()\n                            continuation.resume(returning: result)\n                        } catch {\n                            continuation.resume(throwing: error)\n                        }\n                    }\n\n                    switch access {\n                    case .read:\n                        coordinator.coordinate(readingItemAt: self.rootDirectoryURL, options: [], error: &coordError, byAccessor: accessor)\n                    case .write:\n                        coordinator.coordinate(writingItemAt: self.rootDirectoryURL, options: [], error: &coordError, byAccessor: accessor)\n                    }\n\n                    if let error = coordError {\n                        continuation.resume(throwing: error)\n                    }\n                } else {\n                    do {\n                        let result = try block()\n                        continuation.resume(returning: result)\n                    } catch {\n                        continuation.resume(throwing: error)\n                    }\n                }\n            }\n        }\n    }\n\n    // MARK:- Snapshot Exchange\n\n    public func retrieveSnapshotManifest() async throws -> SnapshotManifest? {\n        try await withCheckedThrowingContinuation { continuation in\n            queue.addOperation {\n                let manifestURL = self.snapshotsDirectory.appendingPathComponent(\"manifest.json\")\n                guard self.fileManager.fileExists(atPath: manifestURL.path) else {\n                    continuation.resume(returning: nil)\n                    return\n                }\n                do {\n                    let data = try Data(contentsOf: manifestURL)\n                    let manifest = try JSONDecoder().decode(SnapshotManifest.self, from: data)\n                    continuation.resume(returning: manifest)\n                } catch {\n                    continuation.resume(throwing: error)\n                }\n            }\n        }\n    }\n\n    public func retrieveSnapshotChunk(index: Int) async throws -> Data {\n        try await withCheckedThrowingContinuation { continuation in\n            queue.addOperation {\n                let chunkURL = self.snapshotsDirectory.appendingPathComponent(String(format: \"chunk-%03d\", index))\n                guard self.fileManager.fileExists(atPath: chunkURL.path) else {\n                    continuation.resume(throwing: Error.snapshotChunkMissing(index))\n                    return\n                }\n                do {\n                    let data = try Data(contentsOf: chunkURL)\n                    continuation.resume(returning: data)\n                } catch {\n                    continuation.resume(throwing: error)\n                }\n            }\n        }\n    }\n\n    public func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws {\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            queue.addOperation {\n                do {\n                    // Remove previous snapshot if any\n                    if self.fileManager.fileExists(atPath: self.snapshotsDirectory.path) {\n                        try self.fileManager.removeItem(at: self.snapshotsDirectory)\n                    }\n                    try self.fileManager.createDirectory(at: self.snapshotsDirectory, withIntermediateDirectories: true, attributes: nil)\n\n                    // Write chunks\n                    for i in 0..<manifest.chunkCount {\n                        let chunkData = try chunkProvider(i)\n                        let chunkURL = self.snapshotsDirectory.appendingPathComponent(String(format: \"chunk-%03d\", i))\n                        try chunkData.write(to: chunkURL)\n                    }\n\n                    // Write manifest\n                    let manifestData = try JSONEncoder().encode(manifest)\n                    let manifestURL = self.snapshotsDirectory.appendingPathComponent(\"manifest.json\")\n                    try manifestData.write(to: manifestURL)\n\n                    continuation.resume()\n                } catch {\n                    continuation.resume(throwing: error)\n                }\n            }\n        }\n    }\n\n    // MARK:- File Presenter\n\n    public var presentedItemURL: URL? {\n        return rootDirectoryURL\n    }\n\n    public var presentedItemOperationQueue: OperationQueue {\n        return queue\n    }\n\n    public func presentedItemDidChange() {\n        self.newVersionsContinuation.yield(())\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Exchanges/MemoryExchange.swift",
    "content": "//\n//  MemoryExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\n\npublic actor MemoryExchange: Exchange {\n\n    nonisolated public let store: Store\n\n    nonisolated public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    nonisolated public var restorationState: Data? {\n        get { return nil }\n        set {}\n    }\n\n    private var versionsByIdentifier: [Version.ID: Version] = [:]\n    private var valueChangesByVersionIdentifier: [Version.ID: [Value.Change]] = [:]\n\n    public init(store: Store) {\n        self.store = store\n        let (stream, continuation) = AsyncStream<Void>.makeStream()\n        self.newVersionsAvailable = stream\n        self.newVersionsContinuation = continuation\n    }\n\n    public func prepareToRetrieve() async throws {\n    }\n\n    public func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        return Array(versionsByIdentifier.keys)\n    }\n\n    public func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        var versions: [Version] = []\n        for id in versionIds {\n            if let version = versionsByIdentifier[id] {\n                versions.append(version)\n            }\n        }\n        return versions\n    }\n\n    public func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        var result: [Version.ID: [Value.Change]] = [:]\n        for id in versionIds {\n            if let changes = valueChangesByVersionIdentifier[id] {\n                result[id] = changes\n            }\n        }\n        return result\n    }\n\n    public func prepareToSend() async throws {\n    }\n\n    public func send(versionChanges: [VersionChanges]) async throws {\n        for (version, valueChanges) in versionChanges {\n            versionsByIdentifier[version.id] = version\n            valueChangesByVersionIdentifier[version.id] = valueChanges\n        }\n        newVersionsContinuation.yield(())\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/Exchanges/MultipeerExchange.swift",
    "content": "//\n//  MultipeerExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\n/// Protocol abstracting peer-to-peer data transport (e.g., wraps MCSession).\n/// Apps implement this to bridge their MultipeerConnectivity session.\npublic protocol PeerTransport: AnyObject {\n    func send(_ data: Data, toPeer peerID: String) throws\n}\n\n/// An Exchange that syncs between two peers via a direct data channel.\n///\n/// The app must call `receiveData(_:)` from its MCSessionDelegate (or equivalent)\n/// whenever data arrives from the peer. MultipeerExchange handles the request-response\n/// protocol internally.\n///\n/// Each peer runs its own MultipeerExchange instance. One peer's `send()`/`retrieve()`\n/// sends requests to the other peer, which responds automatically via `handleRequest`.\npublic class MultipeerExchange: Exchange {\n\n    public enum Error: Swift.Error {\n        case transportUnavailable\n        case timeout\n        case invalidMessage\n        case requestFailed(String)\n    }\n\n    // MARK: - PeerMessage\n\n    struct PeerMessage: Codable {\n        let id: String\n        let type: MessageType\n        var requestId: String?\n        var payload: Data?\n\n        enum MessageType: String, Codable {\n            // Requests (client → server)\n            case requestVersionIds\n            case requestVersions\n            case requestValueChanges\n\n            // Responses (server → client)\n            case responseVersionIds\n            case responseVersions\n            case responseValueChanges\n\n            // Push (no response expected)\n            case pushVersionChanges\n\n            // Error\n            case errorResponse\n        }\n    }\n\n    // MARK: - Properties\n\n    nonisolated public let store: Store\n    public let peerID: String\n    public weak var transport: PeerTransport?\n\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    public var restorationState: Data? {\n        get { return nil }\n        set {}\n    }\n\n    /// Actor that serializes internal state access\n    private let state = State()\n\n    private actor State {\n        var pendingRequests: [String: CheckedContinuation<Data, Swift.Error>] = [:]\n\n        func addRequest(id: String, continuation: CheckedContinuation<Data, Swift.Error>) {\n            pendingRequests[id] = continuation\n        }\n\n        func removeRequest(id: String) -> CheckedContinuation<Data, Swift.Error>? {\n            pendingRequests.removeValue(forKey: id)\n        }\n    }\n\n    private let requestTimeout: UInt64 = 30_000_000_000 // 30 seconds in nanoseconds\n\n    // MARK: - Init\n\n    public init(store: Store, peerID: String, transport: PeerTransport?) {\n        self.store = store\n        self.peerID = peerID\n        self.transport = transport\n        let (stream, continuation) = AsyncStream<Void>.makeStream()\n        self.newVersionsAvailable = stream\n        self.newVersionsContinuation = continuation\n    }\n\n    // MARK: - Incoming Data (call from MCSessionDelegate)\n\n    /// Call this method from your MCSessionDelegate's `session(_:didReceive:fromPeer:)`.\n    public func receiveData(_ data: Data) {\n        guard let message = try? JSONDecoder().decode(PeerMessage.self, from: data) else {\n            return\n        }\n\n        Task {\n            switch message.type {\n            case .requestVersionIds, .requestVersions, .requestValueChanges:\n                await handleRequest(message)\n            case .responseVersionIds, .responseVersions, .responseValueChanges:\n                await handleResponse(message)\n            case .pushVersionChanges:\n                await handlePush(message)\n            case .errorResponse:\n                await handleResponse(message)\n            }\n        }\n    }\n\n    // MARK: - Exchange Protocol\n\n    public func prepareToRetrieve() async throws {}\n    public func prepareToSend() async throws {}\n\n    public func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        let responseData = try await sendRequest(type: .requestVersionIds, payload: nil)\n        return try JSONDecoder().decode([Version.ID].self, from: responseData)\n    }\n\n    public func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        guard !versionIds.isEmpty else { return [] }\n        let payload = try JSONEncoder().encode(versionIds)\n        let responseData = try await sendRequest(type: .requestVersions, payload: payload)\n        return try JSONDecoder().decode([Version].self, from: responseData)\n    }\n\n    public func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        guard !versionIds.isEmpty else { return [:] }\n        let payload = try JSONEncoder().encode(versionIds)\n        let responseData = try await sendRequest(type: .requestValueChanges, payload: payload)\n        return try JSONDecoder().decode([Version.ID: [Value.Change]].self, from: responseData)\n    }\n\n    public func send(versionChanges: [VersionChanges]) async throws {\n        guard !versionChanges.isEmpty else { return }\n        let codable = versionChanges.map { CodableVersionChanges(version: $0.version, valueChanges: $0.valueChanges) }\n        let payload = try JSONEncoder().encode(codable)\n        let message = PeerMessage(id: UUID().uuidString, type: .pushVersionChanges, payload: payload)\n        try sendMessage(message)\n    }\n\n    // MARK: - Request-Response\n\n    private func sendRequest(type: PeerMessage.MessageType, payload: Data?) async throws -> Data {\n        guard transport != nil else {\n            throw Error.transportUnavailable\n        }\n\n        let requestId = UUID().uuidString\n        let message = PeerMessage(id: requestId, type: type, payload: payload)\n\n        return try await withCheckedThrowingContinuation { continuation in\n            Task {\n                await state.addRequest(id: requestId, continuation: continuation)\n\n                do {\n                    try sendMessage(message)\n                } catch {\n                    if let cont = await state.removeRequest(id: requestId) {\n                        cont.resume(throwing: error)\n                    }\n                    return\n                }\n\n                // Timeout\n                Task {\n                    try? await Task.sleep(nanoseconds: requestTimeout)\n                    if let cont = await state.removeRequest(id: requestId) {\n                        cont.resume(throwing: Error.timeout)\n                    }\n                }\n            }\n        }\n    }\n\n    private func sendMessage(_ message: PeerMessage) throws {\n        guard let transport = transport else {\n            throw Error.transportUnavailable\n        }\n        let data = try JSONEncoder().encode(message)\n        try transport.send(data, toPeer: peerID)\n    }\n\n    // MARK: - Handling Incoming Messages\n\n    private func handleRequest(_ message: PeerMessage) async {\n        do {\n            let responsePayload: Data\n            let responseType: PeerMessage.MessageType\n\n            switch message.type {\n            case .requestVersionIds:\n                var allIds: [Version.ID] = []\n                store.queryHistory { history in\n                    allIds = history.allVersionIdentifiers\n                }\n                responsePayload = try JSONEncoder().encode(allIds)\n                responseType = .responseVersionIds\n\n            case .requestVersions:\n                guard let payload = message.payload else { throw Error.invalidMessage }\n                let versionIds = try JSONDecoder().decode([Version.ID].self, from: payload)\n                let versions: [Version] = try versionIds.compactMap { try store.version(identifiedBy: $0) }\n                responsePayload = try JSONEncoder().encode(versions)\n                responseType = .responseVersions\n\n            case .requestValueChanges:\n                guard let payload = message.payload else { throw Error.invalidMessage }\n                let versionIds = try JSONDecoder().decode([Version.ID].self, from: payload)\n                var changesByVersion: [Version.ID: [Value.Change]] = [:]\n                for id in versionIds {\n                    changesByVersion[id] = try store.valueChanges(madeInVersionIdentifiedBy: id)\n                }\n                responsePayload = try JSONEncoder().encode(changesByVersion)\n                responseType = .responseValueChanges\n\n            default:\n                return\n            }\n\n            let response = PeerMessage(id: UUID().uuidString, type: responseType, requestId: message.id, payload: responsePayload)\n            try sendMessage(response)\n        } catch {\n            let errorResponse = PeerMessage(\n                id: UUID().uuidString,\n                type: .errorResponse,\n                requestId: message.id,\n                payload: error.localizedDescription.data(using: .utf8)\n            )\n            try? sendMessage(errorResponse)\n        }\n    }\n\n    private func handleResponse(_ message: PeerMessage) async {\n        guard let requestId = message.requestId else { return }\n\n        if let continuation = await state.removeRequest(id: requestId) {\n            if message.type == .errorResponse {\n                let description = message.payload.flatMap { String(data: $0, encoding: .utf8) } ?? \"Unknown error\"\n                continuation.resume(throwing: Error.requestFailed(description))\n            } else {\n                continuation.resume(returning: message.payload ?? Data())\n            }\n        }\n    }\n\n    private func handlePush(_ message: PeerMessage) async {\n        guard let payload = message.payload else { return }\n\n        do {\n            let codable = try JSONDecoder().decode([CodableVersionChanges].self, from: payload)\n            let versionChanges: [VersionChanges] = codable.map { ($0.version, $0.valueChanges) }\n\n            for (version, valueChanges) in versionChanges {\n                do {\n                    try store.addVersion(version, storing: valueChanges)\n                } catch Store.Error.attemptToAddExistingVersion {\n                    // Ignore\n                }\n            }\n\n            newVersionsContinuation.yield(())\n        } catch {\n            log.error(\"Failed to handle push: \\(error)\")\n        }\n    }\n}\n\n// MARK: - Codable Helper\n\nprivate struct CodableVersionChanges: Codable {\n    let version: Version\n    let valueChanges: [Value.Change]\n}\n"
  },
  {
    "path": "Sources/LLVS/General/Cache.swift",
    "content": "//\n//  Cache.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\nimport Synchronization\n\n/// Generational cache. Fills up each generation to a limit, then discards oldest creating a new generation.\n/// When you retrieve a value, it automatically adds that value to the newest generation, to keep it around.\n/// Creating generations is based on the number of values in the latest generation, not on time or data size.\npublic final class Cache<ValueType> {\n\n    private class Generation {\n        private var valuesByIdentifier: [AnyHashable:ValueType] = [:]\n\n        subscript(id: AnyHashable) -> ValueType? {\n            get {\n                return valuesByIdentifier[id]\n            }\n            set(newValue) {\n                valuesByIdentifier[id] = newValue\n            }\n        }\n\n        var count: Int { return valuesByIdentifier.count }\n    }\n\n    private struct State {\n        var generations: [Generation]\n    }\n\n    public let numberOfGenerations: Int\n    public let regenerationLimit: Int\n\n    private let state: Mutex<State>\n\n    public init(numberOfGenerations: Int = 2, regenerationLimit: Int = 1000) {\n        self.numberOfGenerations = max(1, numberOfGenerations)\n        self.regenerationLimit = max(1, regenerationLimit)\n        let generations: [Generation] = .init(repeating: Generation(), count: max(1, numberOfGenerations))\n        self.state = Mutex(State(generations: generations))\n    }\n\n    public func setValue(_ value: ValueType, for identifier: AnyHashable) {\n        state.withLock { state in\n            regenerateIfNeeded(&state)\n            state.generations.first![identifier] = value\n        }\n    }\n\n    public func removeValue(for identifier: AnyHashable) {\n        state.withLock { state in\n            state.generations.forEach { generation in\n                generation[identifier] = nil\n            }\n        }\n    }\n\n    public func value(for identifier: AnyHashable) -> ValueType? {\n        state.withLock { state in\n            if let generation = state.generations.first(where: { $0[identifier] != nil }) {\n                let value = generation[identifier]\n                state.generations.first![identifier] = value // Keep current by adding to most recent generation\n                return value\n            } else {\n                return nil\n            }\n        }\n    }\n\n    public func purgeAllValues() {\n        state.withLock { state in\n            state.generations = .init(repeating: Generation(), count: self.numberOfGenerations)\n        }\n    }\n\n    private func regenerateIfNeeded(_ state: inout State) {\n        let generation = state.generations.first!\n        if generation.count > regenerationLimit {\n            regenerate(&state)\n        }\n    }\n\n    private func regenerate(_ state: inout State) {\n        let _ = state.generations.dropLast()\n        state.generations.insert(Generation(), at: 0)\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/General/DataCompression.swift",
    "content": "//\n//  DataCompression.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\n/// Transparent compression/decompression using LZFSE via Foundation's NSData API.\n/// Compressed data is prefixed with a 4-byte magic header (\"LLZF\") for reliable detection.\n/// Uncompressed data (legacy or too small to benefit) is returned as-is on read.\npublic enum DataCompression {\n\n    /// Magic bytes prepended to compressed data: \"LLZF\"\n    private static let magic: [UInt8] = [0x4C, 0x4C, 0x5A, 0x46]\n\n    /// Minimum data size worth attempting compression.\n    private static let minimumCompressionSize = 64\n\n    /// Compresses data using LZFSE. Returns original data if the input is too small\n    /// or compression doesn't reduce size (accounting for the 4-byte magic header).\n    public static func compress(_ data: Data) -> Data {\n        guard data.count > minimumCompressionSize else { return data }\n        guard let compressed = try? (data as NSData).compressed(using: .lzfse) as Data else { return data }\n        guard compressed.count + magic.count < data.count else { return data }\n        var result = Data(magic)\n        result.append(compressed)\n        return result\n    }\n\n    /// Decompresses data if it starts with the \"LLZF\" magic header.\n    /// Data without the header (legacy uncompressed or small data) is returned as-is.\n    public static func decompressIfNeeded(_ data: Data) -> Data {\n        guard data.count > magic.count else { return data }\n        let s = data.startIndex\n        guard data[s] == magic[0], data[s+1] == magic[1],\n              data[s+2] == magic[2], data[s+3] == magic[3] else {\n            return data\n        }\n        let compressed = Data(data[(s + magic.count)...])\n        guard let decompressed = try? (compressed as NSData).decompressed(using: .lzfse) as Data else {\n            return data\n        }\n        return decompressed\n    }\n}\n\n// MARK: - Value.Change Helpers\n\ninternal extension DataCompression {\n\n    /// Compresses the data payload within Value.Change insert/update cases.\n    static func compressValueChanges(_ changes: [Value.Change]) -> [Value.Change] {\n        return changes.map { change in\n            switch change {\n            case .insert(let value):\n                var v = value\n                v.data = compress(v.data)\n                return .insert(v)\n            case .update(let value):\n                var v = value\n                v.data = compress(v.data)\n                return .update(v)\n            case .remove, .preserve, .preserveRemoval:\n                return change\n            }\n        }\n    }\n\n    /// Decompresses the data payload within Value.Change insert/update cases.\n    static func decompressValueChanges(_ changes: [Value.Change]) -> [Value.Change] {\n        return changes.map { change in\n            switch change {\n            case .insert(let value):\n                var v = value\n                v.data = decompressIfNeeded(v.data)\n                return .insert(v)\n            case .update(let value):\n                var v = value\n                v.data = decompressIfNeeded(v.data)\n                return .update(v)\n            case .remove, .preserve, .preserveRemoval:\n                return change\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/General/DynamicTaskBatcher.swift",
    "content": "//\n//  DynamicTaskBatcher.swift\n//\n//\n//  Created by Drew McCormack on 06/03/2020.\n//\n\nimport Foundation\n\n/// Generates batches for a fixed number of asynchronous tasks, based on a cost criterion for each task.\n/// This is useful for asynchronously processing an array of tasks, where you have a cost function for each task, and want batches that try to avoid having too much cost.\n/// It can also dynamically adjust if a batch is not suitable, by growing and repeating the batch.\npublic final class DynamicTaskBatcher {\n\n    public enum Error: Swift.Error {\n        case couldNotFurtherGrowFailingBatch\n    }\n\n    /// The outcome of a single batch execution.\n    public enum BatchCompletionOutcome {\n        /// Definitively succeeded or failed. Failure causes completion block to be called with error\n        case definitive(Result<Void, Swift.Error>)\n\n        /// A half failure. Use this to indicate the batch did not succeed, but should be retried after growing.\n        /// If it is not possible to grow the batch further, completion is called with error.\n        case growBatchAndReexecute\n    }\n\n    public typealias TaskCostEvaluator = (_ index: Int) -> Float\n    public typealias BatchExecuter = @Sendable (_ batchIndexRange: Range<Int>) async throws -> BatchCompletionOutcome\n\n    public let numberOfTasks: Int\n\n    /// Func that estimates the cost of a given task. Cost is between 0 and 1.\n    /// A cost of 1 will result in a batch with only that one task. Task costs are tallied until\n    /// they exceed 1, at which point the batch is complete and run.\n    public let taskCostEvaluator: TaskCostEvaluator\n\n    /// Executes a batch\n    public let batchExecuter: BatchExecuter\n\n    public init(numberOfTasks: Int, taskCostEvaluator: @escaping TaskCostEvaluator, batchExecuter: @escaping BatchExecuter) {\n        self.numberOfTasks = numberOfTasks\n        self.taskCostEvaluator = taskCostEvaluator\n        self.batchExecuter = batchExecuter\n    }\n\n    // MARK: Execution\n\n    private var currentBatchSize: Int = -1\n    private var completedCount: Int = 0\n    private var previousBatchNeedsReexecutionAfterGrowth = false\n\n    public func start() async throws {\n        self.currentBatchSize = -1\n        self.completedCount = 0\n        self.previousBatchNeedsReexecutionAfterGrowth = false\n        try await startNextBatch()\n    }\n\n    private func calculateNextBatchSize() -> Int {\n        let numberRemaining = numberOfTasks-completedCount\n        defer { previousBatchNeedsReexecutionAfterGrowth = false }\n\n        guard completedCount < numberOfTasks else { return 0 }\n        guard !previousBatchNeedsReexecutionAfterGrowth else {\n            return min(currentBatchSize+1, numberRemaining)\n        }\n\n        // Increase index until the accumulated cost is greater than 1\n        var i = completedCount\n        var cost: Float = 0\n        while i < numberOfTasks {\n            cost += taskCostEvaluator(i)\n            if cost >= 1.0 { break }\n            i += 1\n        }\n\n        let newBatchSize = max(1, i-completedCount)\n        return min(newBatchSize, numberRemaining)\n    }\n\n    private func startNextBatch() async throws {\n        let numberRemaining = numberOfTasks-completedCount\n\n        guard numberRemaining > 0 else {\n            return\n        }\n\n        if previousBatchNeedsReexecutionAfterGrowth, completedCount + currentBatchSize == numberOfTasks  {\n            // Can't grow the batch anymore, and it is still failing. So fail outright\n            throw Error.couldNotFurtherGrowFailingBatch\n        }\n\n        currentBatchSize = calculateNextBatchSize()\n\n        let outcome = try await batchExecuter(completedCount..<completedCount+currentBatchSize)\n        switch outcome {\n        case .definitive(let result):\n            switch result {\n            case .success:\n                self.completedCount += self.currentBatchSize\n                try await self.startNextBatch()\n            case .failure(let error):\n                throw error\n            }\n        case .growBatchAndReexecute:\n            self.previousBatchNeedsReexecutionAfterGrowth = true\n            try await self.startNextBatch()\n        }\n    }\n\n}\n"
  },
  {
    "path": "Sources/LLVS/General/General.swift",
    "content": "//\n//  General.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 04/11/2018.\n//\n\nimport Foundation\nimport Synchronization\n\npublic extension Result {\n    var value: Success? {\n        guard case let .success(value) = self else { return nil }\n        return value\n    }\n    \n    var voidResult: Result<Void, Error> {\n        switch self {\n        case let .failure(error):\n            return .failure(error)\n        case .success:\n            return .success(())\n        }\n    }\n\n    var isSuccess: Bool {\n        switch self {\n        case .failure:\n            return false\n        case .success:\n            return true\n        }\n    }\n}\n\npublic extension ClosedRange where Bound == Int {\n    func split(intoRangesOfLength size: Bound) -> [ClosedRange] {\n        let end = upperBound+1\n        return stride(from: lowerBound, to: end, by: size).map {\n            ClosedRange(uncheckedBounds: (lower: $0, upper: Swift.min($0+size-1, upperBound)))\n        }\n    }\n}\n\n@propertyWrapper\npublic struct Atomic<Value: Sendable> {\n\n    private final class Storage: @unchecked Sendable {\n        let mutex: Mutex<Value>\n        init(_ value: Value) { self.mutex = Mutex(value) }\n    }\n\n    private let storage: Storage\n\n    public init(wrappedValue value: Value) {\n        self.storage = Storage(value)\n    }\n\n    public var wrappedValue: Value {\n      get { storage.mutex.withLock { $0 } }\n      set { storage.mutex.withLock { $0 = newValue } }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVS/General/Log.swift",
    "content": "//\n//  Log.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 10/05/19.\n//\n\nimport Foundation\nimport os\n\npublic let log = Log()\n\npublic class Log {\n\n    public enum Level : Int, Comparable {\n        case none\n        case error\n        case warning\n        case trace\n        case verbose\n\n        public var stringValue: String {\n            switch self {\n            case .none:\n                return \"N\"\n            case .error:\n                return \"E\"\n            case .warning:\n                return \"W\"\n            case .trace:\n                return \"T\"\n            case .verbose:\n                return \"V\"\n            }\n        }\n    }\n\n    public var level = Level.none\n\n    @inline(__always) public final func verbose(_ messageClosure: @autoclosure () -> String, path: StaticString = #file, function: StaticString = #function, line: Int = #line) {\n        if level >= .verbose {\n            Log.append(messageClosure(), level: .verbose, path: path, function: function, line: line)\n        }\n    }\n\n    @inline(__always) public final func trace(_ messageClosure: @autoclosure () -> String, path: StaticString = #file, function: StaticString = #function, line: Int = #line) {\n        if level >= .trace {\n            Log.append(messageClosure(), level: .trace, path: path, function: function, line: line)\n        }\n    }\n\n    @inline(__always) public final func warning(_ messageClosure: @autoclosure () -> String, path: StaticString = #file, function: StaticString = #function, line: Int = #line) {\n        if level >= .warning {\n            Log.append(messageClosure(), level: .warning, path: path, function: function, line: line)\n        }\n    }\n\n    @inline(__always) public final func error(_ messageClosure: @autoclosure () -> String, path: StaticString = #file, function: StaticString = #function, line: Int = #line) {\n        if level >= .error {\n            Log.append(messageClosure(), level: .error, path: path, function: function, line: line)\n        }\n    }\n\n    @inline(__always) public final class func append(_ messageClosure: @autoclosure () -> String, level: Level, path: StaticString, function: StaticString, line: Int = #line) {\n        let filename = (String(describing: path) as NSString).lastPathComponent\n        let text = \"\\(level.rawValue) \\(filename)(\\(line)) : \\(function) : \\(messageClosure())\"\n        os_log(\"%{public}@\", text)\n    }\n}\n\n@inline(__always) public func <(a: Log.Level, b: Log.Level) -> Bool {\n    return a.rawValue < b.rawValue\n}\n"
  },
  {
    "path": "Sources/LLVS/Utilities/ArrayDiff.swift",
    "content": "//\n//  ArrayMerge.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 02/04/2019.\n//\n\nimport Foundation\n\npublic extension Array where Element: Equatable {\n    \n    func diff(leadingTo newArray: [Element]) -> ArrayDiff<Element> {\n        return ArrayDiff<Element>(originalValues: self, finalValues: newArray)\n    }\n    \n    func applying(_ arrayDiff: ArrayDiff<Element>) -> [Element] {\n        var new = self\n        new.apply(arrayDiff)\n        return new\n    }\n    \n    mutating func apply(_ arrayDiff: ArrayDiff<Element>) {\n        for diff in arrayDiff.incrementalChanges {\n            apply(diff)\n        }\n    }\n    \n    mutating func apply(_ diff: ArrayDiff<Element>.IncrementalChange) {\n        switch diff {\n        case let .delete(index, _):\n            guard indices ~= index else { return }\n            remove(at: index)\n        case let .insert(finalIndex, value):\n            let insertIndex = Swift.min(finalIndex, count)\n            insert(value, at: insertIndex)\n        }\n    }\n    \n}\n\n/// Uses longest common subsequence algorithm to find difference between two arrays.\n/// Can be used to update to take the deletions and insertions\n/// applied to one array, and apply them to a related array.\n/// See https://en.wikipedia.org/wiki/Longest_common_subsequence_problem\npublic struct ArrayDiff<T: Equatable> {\n    \n    /// IncrementalChange indicates a change to the original array.\n    /// Indexes of deletions are relative to the original indexes of the original array.\n    /// Indexes of insertions are given relative both the original and final array.\n    public enum IncrementalChange: Equatable {\n        case insert(finalIndex: Int, value: T)\n        case delete(originalIndex: Int, value: T)\n        \n        public var isDeletion: Bool {\n            if case .delete = self { return true }\n            return false\n        }\n        \n        public var isInsertion: Bool {\n            if case .insert = self { return true }\n            return false\n        }\n        \n        public var index: Int {\n            switch self {\n            case let .delete(index, _), let .insert(index, _):\n                return index\n            }\n        }\n    }\n    \n    /// Changes are ordered so that you can apply them in order to the original array,\n    /// and end up with the final array. Deletions come first, indexes according to the\n    /// original array. They are in reversed order, applying to the end first.\n    /// The insertions are next, with the indexes corresponding to the final array.\n    /// They apply from the beginning toward the end, ie, standard order.\n    public private(set) var incrementalChanges: [IncrementalChange] = []\n    \n    public init(withChanges incrementalChanges: [IncrementalChange]) {\n        self.incrementalChanges = incrementalChanges\n    }\n    \n    public init(originalValues: [T], finalValues: [T]) {\n        let lcs = LongestCommonSubsequence(originalValues: originalValues, finalValues: finalValues)\n        self.incrementalChanges = lcs.incrementalChanges\n    }\n    \n    /// Type used to stage intermediate form of merged changes\n    private struct MergedChange {\n        enum Position {\n            case first, second\n        }\n        var deletions: [Position:IncrementalChange?] = [.first: nil, .second: nil]\n        var insertions: [Position:[IncrementalChange]] = [.first: [], .second: []]\n    }\n    \n    /// Creates a new diff from two existing ones, by merging them. Can be used for a 3 way merge.\n    /// Can pass a merge policy if needed to handle case where two insertions conflict.\n    public init(merging first: ArrayDiff, with second: ArrayDiff) {\n        var mergedChangesByOriginalIndex: [Int:MergedChange] = [:]\n        \n        func addDeletions(in changes: [IncrementalChange], position: MergedChange.Position) {\n            for change in changes {\n                switch change {\n                case let .delete(i, _):\n                    var m = mergedChangesByOriginalIndex[i, default: MergedChange()]\n                    m.deletions[position] = change\n                    mergedChangesByOriginalIndex[i] = m\n                case .insert:\n                    break\n                }\n            }\n        }\n        addDeletions(in: first.incrementalChanges, position: .first)\n        addDeletions(in: second.incrementalChanges, position: .second)\n        \n        func addInsertions(from changes: [IncrementalChange], position: MergedChange.Position) {\n            let insertions = changes.filter({ $0.isInsertion })\n            var originalIndex = -1\n            var finalIndex = -1\n            for (i, insertion) in insertions.enumerated() {\n                let insertionsContiguous = i > 0 && (insertions[i].index - insertions[i-1].index == 1)\n                while !insertionsContiguous, insertion.index != finalIndex {\n                    let deletions = mergedChangesByOriginalIndex[originalIndex]?.deletions\n                    if  deletions?[position] != nil { finalIndex -= 1 }\n                    originalIndex += 1\n                    finalIndex += 1\n                }\n                var m = mergedChangesByOriginalIndex[originalIndex, default: MergedChange()]\n                m.insertions[position]!.append(insertion)\n                mergedChangesByOriginalIndex[originalIndex] = m\n                finalIndex += 1\n            }\n        }\n        addInsertions(from: first.incrementalChanges, position: .first)\n        addInsertions(from: second.incrementalChanges, position: .second)\n\n        // Build result from merged changes\n        var resultDeletions: [IncrementalChange] = []\n        var resultInsertions: [IncrementalChange] = []\n        var finalIndex = -1\n        var previousOriginalIndex = -1\n        for originalIndex in mergedChangesByOriginalIndex.keys.sorted() {\n            let mergedChange = mergedChangesByOriginalIndex[originalIndex]!\n            \n            // Update final index for any items with no changes\n            finalIndex += originalIndex - previousOriginalIndex\n            \n            // Add deletion\n            if let change = mergedChange.deletions[.first]! ?? mergedChange.deletions[.second]!, case let .delete(_, value) = change {\n                resultDeletions.append(.delete(originalIndex: originalIndex, value: value))\n            }\n            \n            // Add insertions\n            for case let .insert(_, value) in mergedChange.insertions[.first]! + mergedChange.insertions[.second]! {\n                resultInsertions.append(.insert(finalIndex: finalIndex, value: value))\n                finalIndex += 1\n            }\n            \n            previousOriginalIndex = originalIndex\n        }\n        \n        self.init(withChanges: resultDeletions.reversed() + resultInsertions)\n    }\n}\n\ninternal final class LongestCommonSubsequence<T: Equatable> {\n    typealias Change = ArrayDiff<T>.IncrementalChange\n    \n    public let originalValues: [T]\n    public let finalValues: [T]\n    \n    public private(set) var originalIndexesOfCommonElements: [Int] = []\n    public private(set) var finalIndexesOfCommonElements: [Int] = []\n    \n    public private(set) var incrementalChanges: [Change] = []\n\n    public var length: Int {\n        guard !originalValues.isEmpty, !finalValues.isEmpty else { return 0 }\n        return table[(originalValues.count-1, finalValues.count-1)].length\n    }\n    \n    private let table: Table\n    \n    public init(originalValues: [T], finalValues: [T]) {\n        self.originalValues = originalValues\n        self.finalValues = finalValues\n        self.table = Table(originalLength: self.originalValues.count, newLength: self.finalValues.count)\n        fillTable()\n        findLongestSubsequence()\n    }\n    \n    private func coordinate(to neighbor: Table.Neighbor, of coordinate: Table.Coordinate) -> Table.Coordinate {\n        return neighbor.coordinate(from: coordinate)\n    }\n    \n    private func fillTable() {\n        for row in 0..<originalValues.count {\n            for col in 0..<finalValues.count {\n                let coord = (row, col)\n                let left = table[coordinate(to: .left, of: coord)]\n                let top = table[coordinate(to: .top, of: coord)]\n                var subsequence = table[coord]\n                if originalValues[row] == finalValues[col] {\n                    let topLeft = table[coordinate(to: .topLeft, of: coord)]\n                    subsequence.contributors = [.topLeft]\n                    subsequence.length = topLeft.length+1\n                } else if left.length > top.length {\n                    subsequence.contributors = [.left]\n                    subsequence.length = left.length\n                } else if top.length > left.length {\n                    subsequence.contributors = [.top]\n                    subsequence.length = top.length\n                } else {\n                    subsequence.contributors = [.top, .left]\n                    subsequence.length = top.length\n                }\n                table[coord] = subsequence\n            }\n        }\n    }\n    \n    private func findLongestSubsequence() {\n        // Begin at end and walk back to origin\n        var deletions: [Change] = []\n        var insertions: [Change] = []\n        var coord = (originalValues.count-1, finalValues.count-1)\n        while coord.0 > -1 || coord.1 > -1 {\n            let sub = table[coord]\n            \n            // Determine the preferred neighbor.\n            // Update coord to that neighbor when finished this iteration.\n            var preferred: Table.Neighbor?\n            defer { coord = preferred!.coordinate(from: coord) }\n            \n            // Try to move diagonally to top-left\n            preferred = sub.contributors.first { neighbor in\n                let neighborSub = table[neighbor.coordinate(from: coord)]\n                return neighborSub.length < sub.length\n            }\n            guard preferred == nil else {\n                originalIndexesOfCommonElements.insert(coord.0, at: 0)\n                finalIndexesOfCommonElements.insert(coord.1, at: 0)\n                continue\n            }\n            \n            // Otherwise pick first option\n            preferred = sub.contributors.first\n            switch preferred! {\n            case .left:\n                if coord.1 == -1, coord.0 == -1 { break }\n                let delta: Change = .insert(finalIndex: coord.1, value: finalValues[coord.1])\n                insertions.insert(delta, at: 0)\n            case .top:\n                if coord.1 == -1, coord.0 == -1 { break }\n                let delta: Change = .delete(originalIndex: coord.0, value: originalValues[coord.0])\n                deletions.insert(delta, at: 0)\n            case .topLeft:\n                fatalError()\n            }\n        }\n        \n        // Order changes so that deletions come before insertions, and deletions begin at\n        // the end of the array. In this way, you can apply\n        // the changes in order to transform from the original to the final array, and not\n        // have to be concerned with indexes.\n        incrementalChanges = deletions.reversed() + insertions\n    }\n}\n\nfileprivate extension LongestCommonSubsequence {\n    \n    /// Memoization table\n    final class Table: CustomDebugStringConvertible {\n        \n        typealias Coordinate = (original: Int, new: Int)\n        \n        enum Neighbor: String, CustomDebugStringConvertible {\n            case left\n            case top\n            case topLeft\n            \n            var offset: Coordinate {\n                switch self {\n                case .left:\n                    return (0,-1)\n                case .top:\n                    return (-1,0)\n                case .topLeft:\n                    return (-1,-1)\n                }\n            }\n            \n            func coordinate(from index: Coordinate) -> Coordinate {\n                let offset = self.offset\n                return (original: index.original + offset.original, new: index.new + offset.new)\n            }\n            \n            var debugDescription: String {\n                return self.rawValue\n            }\n        }\n        \n        struct Subsequence {\n            typealias Length = Int\n            var length: Length = 0\n            var contributors: [Neighbor] = []\n        }\n        \n        let originalLength: Int\n        let newLength: Int\n        private var subsequences: [Subsequence]\n        \n        init(originalLength: Int, newLength: Int) {\n            self.originalLength = originalLength\n            self.newLength = newLength\n            self.subsequences = .init(repeating: Subsequence(), count: (originalLength+1) * (newLength+1))\n            for row in 0..<originalLength {\n                self[(row,-1)] = Subsequence(length: 0, contributors: [.top])\n            }\n            for col in 0..<newLength {\n                self[(-1,col)] = Subsequence(length: 0, contributors: [.left])\n            }\n        }\n        \n        /// Coordinates correspond to the indexes in the original and new arrays.\n        /// The storage elements themselves begin at -1, but that is an internal detail.\n        subscript(coordinates: (original: Int, new: Int)) -> Subsequence {\n            get {\n                let i = (coordinates.original+1) * (newLength+1) + (coordinates.new+1)\n                return subsequences[i]\n            }\n            set(newValue) {\n                let i = (coordinates.original+1) * (newLength+1) + (coordinates.new+1)\n                subsequences[i] = newValue\n            }\n        }\n        \n        var debugDescription: String {\n            var result = \"\"\n            for row in -1..<originalLength {\n                var str = \"\\n\"\n                for col in -1..<newLength {\n                    let sub = self[(row,col)]\n                    str += \"(\\(sub.length), \\(sub.contributors))\".padding(toLength: 18, withPad: \" \", startingAt: 0)\n                }\n                result += str\n            }\n            return result\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSBox/BoxExchange.swift",
    "content": "//\n//  BoxExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\nimport LLVS\nimport BoxSdkGen\n\n/// An Exchange that syncs versions via the Box Swift SDK.\n///\n/// Data is stored inside a configurable Box folder, organized as:\n/// - `versions/` — JSON-encoded version metadata, one file per version\n/// - `changes/` — JSON-encoded value changes, one file per version\n///\n/// Uses `BoxClient` from the official Box SDK for all API operations.\n/// The caller provides an authenticated `BoxClient` (e.g. via `BoxDeveloperTokenAuth`\n/// or `BoxCCGAuth`).\npublic class BoxExchange: FolderBasedExchange {\n\n    public typealias FileID = String\n    public typealias FolderID = String\n\n    public enum Error: Swift.Error {\n        case downloadFailed\n        case folderNotFound\n    }\n\n    public let store: Store\n\n    /// The Box client used for API calls.\n    public let client: BoxClient\n\n    /// The Box folder ID that serves as the root for LLVS data.\n    public let rootFolderID: String\n\n    @Atomic private var restoration = RestorationInfo()\n\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    public var restorationState: Data? {\n        get { try? JSONEncoder().encode(restoration) }\n        set {\n            if let data = newValue, let info = try? JSONDecoder().decode(RestorationInfo.self, from: data) {\n                restoration = info\n            }\n        }\n    }\n\n    fileprivate lazy var temporaryDirectory: URL = {\n        let result = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)\n        try? FileManager.default.createDirectory(at: result, withIntermediateDirectories: true, attributes: nil)\n        return result\n    }()\n\n    /// - Parameters:\n    ///   - store: The LLVS store to sync.\n    ///   - client: An authenticated `BoxClient` instance.\n    ///   - rootFolderID: The Box folder ID to use as root for LLVS data.\n    public init(store: Store, client: BoxClient, rootFolderID: String) {\n        self.store = store\n        self.client = client\n        self.rootFolderID = rootFolderID\n        let (stream, continuation) = AsyncStream<Void>.makeStream()\n        self.newVersionsAvailable = stream\n        self.newVersionsContinuation = continuation\n    }\n\n    // MARK: - Prepare\n\n    public func prepareToRetrieve() async throws {\n        try await ensureFoldersExist()\n    }\n\n    public func prepareToSend() async throws {\n        try await ensureFoldersExist()\n    }\n\n    // MARK: - FolderBasedExchange\n\n    public var versionsFolderID: String? { restoration.versionsFolderID }\n    public var changesFolderID: String? { restoration.changesFolderID }\n\n    public func notifyNewVersionsAvailable() {\n        newVersionsContinuation.yield(())\n    }\n\n    public func listFiles(inFolder folderID: String) async throws -> [String: String] {\n        let items = try await listAllItems(inFolder: folderID)\n        var fileMap: [String: String] = [:]\n        for item in items where !item.isFolder {\n            fileMap[item.name] = item.id\n        }\n        return fileMap\n    }\n\n    public func downloadData(forFile fileID: String) async throws -> Data {\n        let tempURL = temporaryDirectory.appendingPathComponent(UUID().uuidString)\n        defer { try? FileManager.default.removeItem(at: tempURL) }\n        guard let savedURL = try await client.downloads.downloadFile(fileId: fileID, downloadDestinationUrl: tempURL) else {\n            throw Error.downloadFailed\n        }\n        return try Data(contentsOf: savedURL)\n    }\n\n    public func uploadData(_ data: Data, named name: String, toFolder folderID: String) async throws {\n        let attributes = UploadFileRequestBodyAttributesField(\n            name: name,\n            parent: UploadFileRequestBodyAttributesParentField(id: folderID)\n        )\n        let body = UploadFileRequestBody(\n            attributes: attributes,\n            file: Utils.generateByteStreamFromBuffer(buffer: data)\n        )\n        _ = try await client.uploads.uploadFile(requestBody: body)\n    }\n\n    // MARK: - Box SDK Helpers\n\n    private func ensureFoldersExist() async throws {\n        if restoration.versionsFolderID != nil && restoration.changesFolderID != nil { return }\n\n        let versionsFolderID = try await createSubfolderIfNeeded(named: \"versions\", inFolder: rootFolderID)\n        let changesFolderID = try await createSubfolderIfNeeded(named: \"changes\", inFolder: rootFolderID)\n\n        restoration.versionsFolderID = versionsFolderID\n        restoration.changesFolderID = changesFolderID\n    }\n\n    private func createSubfolderIfNeeded(named name: String, inFolder parentID: String) async throws -> String {\n        let items = try await listAllItems(inFolder: parentID)\n        if let existing = items.first(where: { $0.name == name && $0.isFolder }) {\n            return existing.id\n        }\n\n        let body = CreateFolderRequestBody(\n            name: name,\n            parent: CreateFolderRequestBodyParentField(id: parentID)\n        )\n        let folder = try await client.folders.createFolder(requestBody: body)\n        return folder.id\n    }\n\n    private struct ItemInfo {\n        let id: String\n        let name: String\n        let isFolder: Bool\n    }\n\n    private func listAllItems(inFolder folderID: String) async throws -> [ItemInfo] {\n        var allItems: [ItemInfo] = []\n        var marker: String? = nil\n        repeat {\n            let queryParams = GetFolderItemsQueryParams(usemarker: true, marker: marker, limit: 1000)\n            let items = try await client.folders.getFolderItems(folderId: folderID, queryParams: queryParams)\n            if let entries = items.entries {\n                for entry in entries {\n                    switch entry {\n                    case .fileFull(let file):\n                        if let name = file.name {\n                            allItems.append(ItemInfo(id: file.id, name: name, isFolder: false))\n                        }\n                    case .folderMini(let folder):\n                        if let name = folder.name {\n                            allItems.append(ItemInfo(id: folder.id, name: name, isFolder: true))\n                        }\n                    default:\n                        break\n                    }\n                }\n            }\n            marker = items.nextMarker\n        } while marker != nil\n        return allItems\n    }\n\n    // MARK: - Restoration\n\n    fileprivate struct RestorationInfo: Codable {\n        var versionsFolderID: String?\n        var changesFolderID: String?\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSCloudKit/CloudKitExchange.swift",
    "content": "//\n//  CloudKitExchange\n//  LLVS\n//\n//  Created by Drew McCormack on 16/03/2019.\n//\n\nimport Foundation\nimport CloudKit\nimport LLVS\n\npublic class CloudKitExchange: Exchange {\n\n    public enum CloudDatabaseDescription {\n        case privateDatabaseWithCustomZone(CKContainer, zoneIdentifier: String)\n        case privateDatabaseWithDefaultZone(CKContainer)\n        case publicDatabase(CKContainer)\n        case sharedDatabase(CKContainer, zoneIdentifier: String)\n\n        var database: CKDatabase {\n            switch self {\n            case let .privateDatabaseWithCustomZone(container, _):\n                return container.privateCloudDatabase\n            case let .privateDatabaseWithDefaultZone(container):\n                return container.privateCloudDatabase\n            case let .publicDatabase(container):\n                return container.publicCloudDatabase\n            case let .sharedDatabase(container, _):\n                return container.sharedCloudDatabase\n            }\n        }\n\n        var zoneIdentifier: String? {\n            switch self {\n            case let .privateDatabaseWithCustomZone(_, zoneIdentifier), let .sharedDatabase(_, zoneIdentifier):\n                return zoneIdentifier\n            default:\n                return nil\n            }\n        }\n    }\n\n    public enum Error: Swift.Error {\n        case couldNotGetVersionFromRecord\n        case noZoneFound\n        case invalidValueChangesDataInRecord\n        case snapshotManifestDecodingFailed\n        case snapshotChunkMissing(Int)\n        case snapshotChunkAssetMissing(Int)\n    }\n\n    fileprivate lazy var temporaryDirectory: URL = {\n        let result = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)\n        try? FileManager.default.createDirectory(at: result, withIntermediateDirectories: true, attributes: nil)\n        return result\n    }()\n\n    /// The store the exchange is updating.\n    public var store: Store\n\n    /// Client to inform of updates\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    /// A store identifier identifies the store in the cloud. This allows multiple stores to use a shared zone like the public database.\n    public let storeIdentifier: String\n\n    /// Used only for the private database, when syncing via a custom zone.\n    public let zoneIdentifier: String?\n\n    /// Can be private, shared or public database. For private, it is best to provide a zone identifier.\n    public let database: CKDatabase\n\n    /// The custom zone being used in the private database, if there is one.\n    public let zone: CKRecordZone?\n\n    /// Use to make dependencies when working with a custom zone\n    private let createZoneOperation: CKModifyRecordZonesOperation?\n\n    /// Zone identifier if we are using a custom zone\n    private var zoneID: CKRecordZone.ID? {\n        guard let zoneIdentifier = zoneIdentifier else { return nil }\n        return CKRecordZone.ID(zoneName: zoneIdentifier, ownerName: CKCurrentUserDefaultName)\n    }\n\n    /// Restoration state\n    @Atomic private var restoration: Restoration = .init()\n\n    /// Limit to use for CloudKit fetches. Should be less than actual limit (ie 400)\n    private let cloudKitFetchLimit = 200\n\n    /// For single user syncing, it is best to use a zone. In that case, pass in the private database and a zone identifier.\n    /// Otherwise, you will be using the default  zone in whichever database you pass.\n    public init(with store: Store, storeIdentifier: String, cloudDatabaseDescription: CloudDatabaseDescription) {\n        self.store = store\n        self.storeIdentifier = storeIdentifier\n        self.zoneIdentifier = cloudDatabaseDescription.zoneIdentifier\n        self.database = cloudDatabaseDescription.database\n        self.zone = zoneIdentifier.flatMap { CKRecordZone(zoneName: $0) }\n        (self.newVersionsAvailable, self.newVersionsContinuation) = AsyncStream<Void>.makeStream()\n        if database.databaseScope == .private, let zone = self.zone {\n            self.createZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [zone], recordZoneIDsToDelete: nil)\n            self.database.add(self.createZoneOperation!)\n        } else {\n            self.createZoneOperation = nil\n        }\n    }\n\n    deinit {\n        newVersionsContinuation.finish()\n    }\n\n    /// Remove a zone, if there is one. Otherwise will give error.\n    public func removeZone() async throws {\n        log.trace(\"Removing zone\")\n        guard let zone = zone else {\n            throw Error.noZoneFound\n        }\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            database.delete(withRecordZoneID: zone.zoneID) { zoneID, error in\n                if let error = error {\n                    log.error(\"Removing zone failed: \\(error)\")\n                    continuation.resume(throwing: error)\n                } else {\n                    log.trace(\"Removed zone\")\n                    continuation.resume()\n                }\n            }\n        }\n    }\n}\n\n\n// MARK:- Querying Versions in Cloud\n\nfileprivate extension CloudKitExchange {\n\n    /// Uses the zone changes API. Requires a custom zone.\n    func fetchCloudZoneChanges() async throws {\n        log.trace(\"Fetching cloud changes\")\n\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()\n            config.desiredKeys = []\n            config.previousServerChangeToken = self.restoration.fetchRecordChangesToken\n\n            let operation = CKFetchRecordZoneChangesOperation()\n            operation.recordZoneIDs = [self.zoneID!]\n            operation.configurationsByRecordZoneID = [self.zoneID! : config]\n            operation.addDependency(self.createZoneOperation!)\n            operation.fetchAllChanges = true\n            operation.recordChangedBlock = { record in\n                let versionId = Version.ID(record.recordID.recordName)\n                self.restoration.versionsInCloud.insert(versionId)\n                log.verbose(\"Found record for version: \\(versionId)\")\n            }\n            operation.recordZoneFetchCompletionBlock = { zoneID, token, clientData, moreComing, error in\n                self.restoration.fetchRecordChangesToken = token\n                log.verbose(\"Stored iCloud token: \\(String(describing: token))\")\n            }\n            operation.fetchRecordZoneChangesCompletionBlock = { error in\n                if let error = error as? CKError, error.code == .changeTokenExpired || error.code == .partialFailure {\n                    self.restoration.fetchRecordChangesToken = nil\n                    self.restoration.versionsInCloud = []\n                    log.error(\"iCloud token expired. Cleared cached data\")\n                    // Retry\n                    Task {\n                        do {\n                            try await self.fetchCloudZoneChanges()\n                            continuation.resume()\n                        } catch {\n                            continuation.resume(throwing: error)\n                        }\n                    }\n                } else if let error = error {\n                    continuation.resume(throwing: error)\n                } else {\n                    log.trace(\"Fetched changes\")\n                    continuation.resume()\n                }\n            }\n\n            self.database.add(operation)\n        }\n    }\n\n    enum QueryInfo {\n        case query(CKQuery)\n        case cursor(CKQueryOperation.Cursor)\n\n        func makeQueryOperation() -> CKQueryOperation {\n            switch self {\n            case let .cursor(cursor):\n                return CKQueryOperation(cursor: cursor)\n            case let .query(query):\n                return CKQueryOperation(query: query)\n            }\n        }\n    }\n\n    func makeRecordsQuery() -> CKQuery {\n        let predicate: NSPredicate\n        if let lastQueryDate = restoration.lastQueryDate {\n            predicate = NSPredicate(format: \"storeIdentifier = %@ AND (modificationDate >= %@)\", storeIdentifier, lastQueryDate as NSDate)\n        } else {\n            predicate = NSPredicate(format: \"storeIdentifier = %@\", storeIdentifier)\n        }\n        return CKQuery(recordType: CKRecord.ExchangeType.Version.rawValue, predicate: predicate)\n    }\n\n    /// Get any new version identifiers in cloud\n    func queryDatabaseForNewVersions() async throws {\n        log.trace(\"Querying cloud for new versions\")\n        let query = makeRecordsQuery()\n        do {\n            let records = try await queryDatabase(with: .query(query))\n            let versionIds = records.map { Version.ID($0.recordID.recordName) }\n            self.restoration.versionsInCloud.formUnion(versionIds)\n            let modificationDates = records.map { $0.modificationDate! }\n            self.restoration.lastQueryDate = max(self.restoration.lastQueryDate ?? Date.distantPast, modificationDates.max() ?? Date.distantPast )\n        } catch let error as CKError where error.code == .unknownItem {\n            // Probably don't have data in cloud yet. Ignore error\n            self.restoration.lastQueryDate = Date.distantPast\n        }\n    }\n\n    /// Used when no zone is available. Eg. the public database.\n    func queryDatabase(with queryInfo: QueryInfo) async throws -> [CKRecord] {\n        log.trace(\"Querying cloud changes\")\n\n        return try await withCheckedThrowingContinuation { continuation in\n            let operation = queryInfo.makeQueryOperation()\n            var records: [CKRecord] = []\n            operation.recordFetchedBlock = { record in\n                records.append(record)\n            }\n            operation.queryCompletionBlock = { cursor, error in\n                if let cursor = cursor {\n                    Task {\n                        do {\n                            let moreRecords = try await self.queryDatabase(with: .cursor(cursor))\n                            continuation.resume(returning: records + moreRecords)\n                        } catch {\n                            continuation.resume(throwing: error)\n                        }\n                    }\n                }\n                else {\n                    if let error = error {\n                        log.error(\"Failed to fetch new versions: \\(error)\")\n                        continuation.resume(throwing: error)\n                    } else {\n                        continuation.resume(returning: records)\n                    }\n                }\n            }\n\n            self.database.add(operation)\n        }\n    }\n}\n\n\n// MARK:- Retrieving\n\npublic extension CloudKitExchange {\n\n    func prepareToRetrieve() async throws {\n        log.trace(\"Preparing to retrieve\")\n        if zone != nil {\n            try await fetchCloudZoneChanges()\n        } else {\n            try await queryDatabaseForNewVersions()\n        }\n    }\n\n    func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        log.trace(\"Retrieving versions: \\(versionIds)\")\n\n        guard !versionIds.isEmpty else {\n            return []\n        }\n\n        // Use batches, because CloudKit will give limit error at 400 records\n        let batchRanges = (0...versionIds.count-1).split(intoRangesOfLength: cloudKitFetchLimit)\n        var versions: [Version] = []\n        for range in batchRanges {\n            let batchVersionIds = Array(versionIds[range])\n            let batchVersions = try await retrieve(batchOfVersionsIdentifiedBy: batchVersionIds)\n            versions.append(contentsOf: batchVersions)\n        }\n        return versions\n    }\n\n    /// Assumes that the batch size is less than the limits imposed by CloudKit (ie 400)\n    private func retrieve(batchOfVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version] {\n        log.trace(\"Retrieving versions\")\n        return try await withCheckedThrowingContinuation { continuation in\n            let recordIDs = versionIds.map { CKRecord.ID(recordName: $0.rawValue, zoneID: self.zoneID ?? .default) }\n            let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDs)\n            fetchOperation.desiredKeys = [CKRecord.ExchangeKey.version.rawValue]\n            fetchOperation.fetchRecordsCompletionBlock = { recordsByRecordID, error in\n                guard error == nil else {\n                    continuation.resume(throwing: error!)\n                    return\n                }\n\n                do {\n                    try autoreleasepool {\n                        var versions: [Version] = []\n                        for record in recordsByRecordID!.values {\n                            try autoreleasepool {\n                                if let data = record.exchangeValue(forKey: .version) as? Data, let version = try JSONDecoder().decode([Version].self, from: data).first {\n                                    versions.append(version)\n                                } else {\n                                    throw Error.couldNotGetVersionFromRecord\n                                }\n                            }\n                        }\n                        log.verbose(\"Retrieved versions: \\(versions)\")\n                        continuation.resume(returning: versions)\n                    }\n                } catch {\n                    continuation.resume(throwing: error)\n                }\n            }\n            self.database.add(fetchOperation)\n        }\n    }\n\n    func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {\n        log.verbose(\"Retrieved all versions: \\(restoration.versionsInCloud.map({ $0.rawValue }))\")\n        return Array(restoration.versionsInCloud)\n    }\n\n    func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        log.trace(\"Retrieving value changes for versions: \\(versionIds)\")\n\n        guard !versionIds.isEmpty else {\n            return [:]\n        }\n\n        // Use batches of length 200, because CloudKit will give limit error at 400 records\n        let batchRanges = (0...versionIds.count-1).split(intoRangesOfLength: cloudKitFetchLimit)\n        var changesByVersionId: [Version.ID: [Value.Change]] = [:]\n        for range in batchRanges {\n            let batchVersionIds = Array(versionIds[range])\n            let newChanges = try await retrieve(batchOfValueChangesForVersionsIdentifiedBy: batchVersionIds)\n            changesByVersionId.merge(newChanges) { current, _ in current }\n        }\n        return changesByVersionId\n    }\n\n    /// Retrieves a batch of value changes, assuming batch is smaller than the CloudKit limit\n    private func retrieve(batchOfValueChangesForVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {\n        log.trace(\"Retrieving value changes for versions: \\(versionIds)\")\n        return try await withCheckedThrowingContinuation { continuation in\n            let recordIDs = versionIds.map { CKRecord.ID(recordName: $0.rawValue, zoneID: self.zoneID ?? .default) }\n            let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDs)\n            fetchOperation.desiredKeys = [CKRecord.ExchangeKey.valueChanges.rawValue, CKRecord.ExchangeKey.valueChangesAsset.rawValue]\n            fetchOperation.fetchRecordsCompletionBlock = { recordsByRecordID, error in\n                autoreleasepool {\n                    guard error == nil, let recordsByRecordID = recordsByRecordID else {\n                        continuation.resume(throwing: error!)\n                        return\n                    }\n\n                    do {\n                        let changesByVersion: [(Version.ID, [Value.Change])] = try recordsByRecordID.map { keyValue in\n                            let record = keyValue.value\n                            let recordID = keyValue.key\n                            let data: Data\n                            if let d = record.exchangeValue(forKey: .valueChanges) as? Data {\n                                data = d\n                            } else if let asset = record.exchangeValue(forKey: .valueChangesAsset) as? CKAsset, let url = asset.fileURL {\n                                data = try Data(contentsOf: url)\n                            } else {\n                                throw Error.invalidValueChangesDataInRecord\n                            }\n                            let valueChanges: [Value.Change] = try JSONDecoder().decode([Value.Change].self, from: data)\n                            log.verbose(\"Retrieved value changes for \\(recordID.recordName): \\(valueChanges)\")\n                            return (Version.ID(recordID.recordName), valueChanges)\n                        }\n                        continuation.resume(returning: .init(uniqueKeysWithValues: changesByVersion))\n                    } catch {\n                        log.error(\"Failed to retrieve: \\(error)\")\n                        continuation.resume(throwing: error)\n                    }\n                }\n            }\n            self.database.add(fetchOperation)\n        }\n    }\n}\n\n\n// MARK:- Sending\n\npublic extension CloudKitExchange {\n\n    func prepareToSend() async throws {\n        if zone != nil {\n            try await fetchCloudZoneChanges()\n        } else {\n            try await queryDatabaseForNewVersions()\n        }\n    }\n\n    func send(versionChanges: [VersionChanges]) async throws {\n        log.trace(\"Sending versions: \\(versionChanges.map({ $0.0.id }))\")\n        log.verbose(\"Value changes: \\(versionChanges)\")\n\n        guard !versionChanges.isEmpty else {\n            return\n        }\n\n        // Use batches of length 200, because CloudKit will give limit error at 400 records\n        let batchRanges = (0...versionChanges.count-1).split(intoRangesOfLength: cloudKitFetchLimit)\n        for range in batchRanges {\n            let batchChanges = versionChanges[range]\n            try await send(batchOfVersionChanges: batchChanges)\n        }\n    }\n\n    private func send(batchOfVersionChanges versionChanges: ArraySlice<VersionChanges>) async throws {\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            do {\n                try autoreleasepool {\n                    var tempFileURLs: [URL] = []\n                    let records: [CKRecord] = try versionChanges.map { t in\n                        let version = t.version\n                        let valueChanges = t.valueChanges\n                        let recordID = CKRecord.ID(recordName: version.id.rawValue, zoneID: zoneID ?? .default)\n                        let record = CKRecord(recordType: .init(CKRecord.ExchangeType.Version.rawValue), recordID: recordID)\n                        let versionData = try JSONEncoder().encode([version]) // Use an array, because JSON needs root dict or array\n                        let changesData = try JSONEncoder().encode(valueChanges)\n                        record.setExchangeValue(versionData, forKey: .version)\n                        record.setExchangeValue(storeIdentifier, forKey: .storeIdentifier)\n\n                        // Use an asset for bigger values (>10Kb)\n                        if changesData.count <= 10000 {\n                            record.setExchangeValue(changesData, forKey: .valueChanges)\n                        } else {\n                            let tempFileURL = temporaryDirectory.appendingPathComponent(UUID().uuidString)\n                            try changesData.write(to: tempFileURL)\n                            let asset = CKAsset(fileURL: tempFileURL)\n                            record.setExchangeValue(asset, forKey: .valueChangesAsset)\n                            tempFileURLs.append(tempFileURL)\n                        }\n\n                        return record\n                    }\n\n                    let modifyOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)\n                    modifyOperation.isAtomic = true\n                    modifyOperation.savePolicy = .allKeys\n                    modifyOperation.modifyRecordsCompletionBlock = { _, _, error in\n                        tempFileURLs.forEach { try? FileManager.default.removeItem(at: $0) }\n                        if let error = error {\n                            log.error(\"Failed to send: \\(error)\")\n                            continuation.resume(throwing: error)\n                        } else {\n                            log.trace(\"Succeeded in sending\")\n                            continuation.resume()\n                        }\n                    }\n                    self.database.add(modifyOperation)\n                }\n            } catch {\n                log.error(\"Failed to send: \\(error)\")\n                continuation.resume(throwing: error)\n            }\n        }\n    }\n\n}\n\n\n// MARK:- Snapshot Exchange\n\nextension CloudKitExchange: SnapshotExchange {\n\n    public func retrieveSnapshotManifest() async throws -> SnapshotManifest? {\n        log.trace(\"Retrieving snapshot manifest from CloudKit\")\n        return try await withCheckedThrowingContinuation { continuation in\n            let recordName = \"\\(storeIdentifier)_snapshot_manifest\"\n            let recordID = CKRecord.ID(recordName: recordName, zoneID: zoneID ?? .default)\n            let operation = CKFetchRecordsOperation(recordIDs: [recordID])\n            operation.desiredKeys = [CKRecord.ExchangeKey.snapshotManifest.rawValue]\n            if let createZoneOp = createZoneOperation {\n                operation.addDependency(createZoneOp)\n            }\n            operation.fetchRecordsCompletionBlock = { recordsByID, error in\n                if let ckError = error as? CKError {\n                    if ckError.code == .unknownItem {\n                        continuation.resume(returning: nil)\n                        return\n                    }\n                    if ckError.code == .partialFailure,\n                       let partialErrors = ckError.partialErrorsByItemID,\n                       partialErrors.values.contains(where: { ($0 as? CKError)?.code == .unknownItem }) {\n                        continuation.resume(returning: nil)\n                        return\n                    }\n                    continuation.resume(throwing: ckError)\n                    return\n                }\n                guard let record = recordsByID?[recordID],\n                      let manifestData = record.exchangeValue(forKey: .snapshotManifest) as? Data else {\n                    continuation.resume(returning: nil)\n                    return\n                }\n                do {\n                    let manifest = try JSONDecoder().decode(SnapshotManifest.self, from: manifestData)\n                    log.trace(\"Retrieved snapshot manifest: \\(manifest.snapshotId)\")\n                    continuation.resume(returning: manifest)\n                } catch {\n                    log.error(\"Failed to decode snapshot manifest: \\(error)\")\n                    continuation.resume(throwing: Error.snapshotManifestDecodingFailed)\n                }\n            }\n            self.database.add(operation)\n        }\n    }\n\n    public func retrieveSnapshotChunk(index: Int) async throws -> Data {\n        log.trace(\"Retrieving snapshot chunk \\(index) from CloudKit\")\n        return try await withCheckedThrowingContinuation { continuation in\n            let recordName = \"\\(storeIdentifier)_snapshot_chunk_\\(index)\"\n            let recordID = CKRecord.ID(recordName: recordName, zoneID: zoneID ?? .default)\n            let operation = CKFetchRecordsOperation(recordIDs: [recordID])\n            operation.desiredKeys = [CKRecord.ExchangeKey.snapshotChunkData.rawValue]\n            if let createZoneOp = createZoneOperation {\n                operation.addDependency(createZoneOp)\n            }\n            operation.fetchRecordsCompletionBlock = { recordsByID, error in\n                if let error = error {\n                    log.error(\"Failed to retrieve snapshot chunk \\(index): \\(error)\")\n                    continuation.resume(throwing: Error.snapshotChunkMissing(index))\n                    return\n                }\n                guard let record = recordsByID?[recordID],\n                      let asset = record.exchangeValue(forKey: .snapshotChunkData) as? CKAsset,\n                      let fileURL = asset.fileURL else {\n                    log.error(\"Snapshot chunk \\(index) has no asset\")\n                    continuation.resume(throwing: Error.snapshotChunkAssetMissing(index))\n                    return\n                }\n                do {\n                    let data = try Data(contentsOf: fileURL)\n                    log.trace(\"Retrieved snapshot chunk \\(index): \\(data.count) bytes\")\n                    continuation.resume(returning: data)\n                } catch {\n                    log.error(\"Failed to read snapshot chunk \\(index) asset: \\(error)\")\n                    continuation.resume(throwing: error)\n                }\n            }\n            self.database.add(operation)\n        }\n    }\n\n    public func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws {\n        log.trace(\"Sending snapshot to CloudKit: \\(manifest.chunkCount) chunks\")\n\n        try await deleteExcessSnapshotChunks(keepingCount: manifest.chunkCount)\n        try await uploadSnapshotChunks(manifest: manifest, chunkProvider: chunkProvider)\n        try await uploadSnapshotManifest(manifest)\n    }\n\n    // MARK: Snapshot Helpers\n\n    private func deleteExcessSnapshotChunks(keepingCount: Int) async throws {\n        log.trace(\"Querying for excess snapshot chunks beyond index \\(keepingCount)\")\n        let predicate = NSPredicate(format: \"storeIdentifier = %@ AND snapshotChunkIndex >= %d\", storeIdentifier, keepingCount)\n        let query = CKQuery(recordType: CKRecord.ExchangeType.SnapshotChunk.rawValue, predicate: predicate)\n        do {\n            let records = try await queryDatabase(with: .query(query))\n            if records.isEmpty {\n                log.trace(\"No excess snapshot chunks to delete\")\n            } else {\n                log.trace(\"Deleting \\(records.count) excess snapshot chunks\")\n                try await deleteRecords(records.map { $0.recordID })\n            }\n        } catch let error as CKError where error.code == .unknownItem {\n            // No records to delete\n        }\n    }\n\n    private func deleteRecords(_ recordIDs: [CKRecord.ID]) async throws {\n        guard !recordIDs.isEmpty else { return }\n        let batchRanges = (0...recordIDs.count-1).split(intoRangesOfLength: cloudKitFetchLimit)\n        for range in batchRanges {\n            let batchIDs = Array(recordIDs[range])\n            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n                let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: batchIDs)\n                operation.modifyRecordsCompletionBlock = { _, _, error in\n                    if let error = error {\n                        log.error(\"Failed to delete records: \\(error)\")\n                        continuation.resume(throwing: error)\n                    } else {\n                        continuation.resume()\n                    }\n                }\n                self.database.add(operation)\n            }\n        }\n    }\n\n    private func uploadSnapshotChunks(manifest: SnapshotManifest, chunkProvider: @escaping (Int) throws -> Data) async throws {\n        guard manifest.chunkCount > 0 else { return }\n        let batchRanges = (0...manifest.chunkCount-1).split(intoRangesOfLength: cloudKitFetchLimit)\n        for range in batchRanges {\n            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n                do {\n                    var tempFileURLs: [URL] = []\n                    let records: [CKRecord] = try range.map { index in\n                        let chunkData = try chunkProvider(index)\n                        let recordName = \"\\(self.storeIdentifier)_snapshot_chunk_\\(index)\"\n                        let recordID = CKRecord.ID(recordName: recordName, zoneID: self.zoneID ?? .default)\n                        let record = CKRecord(recordType: .init(CKRecord.ExchangeType.SnapshotChunk.rawValue), recordID: recordID)\n                        record.setExchangeValue(self.storeIdentifier, forKey: .storeIdentifier)\n                        record.setExchangeValue(index, forKey: .snapshotChunkIndex)\n\n                        let tempFileURL = self.temporaryDirectory.appendingPathComponent(UUID().uuidString)\n                        try chunkData.write(to: tempFileURL)\n                        let asset = CKAsset(fileURL: tempFileURL)\n                        record.setExchangeValue(asset, forKey: .snapshotChunkData)\n                        tempFileURLs.append(tempFileURL)\n\n                        return record\n                    }\n                    let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)\n                    operation.savePolicy = .allKeys\n                    operation.modifyRecordsCompletionBlock = { _, _, error in\n                        tempFileURLs.forEach { try? FileManager.default.removeItem(at: $0) }\n                        if let error = error {\n                            log.error(\"Failed to upload snapshot chunks: \\(error)\")\n                            continuation.resume(throwing: error)\n                        } else {\n                            log.trace(\"Uploaded snapshot chunks \\(range)\")\n                            continuation.resume()\n                        }\n                    }\n                    self.database.add(operation)\n                } catch {\n                    log.error(\"Failed to prepare snapshot chunks: \\(error)\")\n                    continuation.resume(throwing: error)\n                }\n            }\n        }\n    }\n\n    private func uploadSnapshotManifest(_ manifest: SnapshotManifest) async throws {\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            do {\n                let manifestData = try JSONEncoder().encode(manifest)\n                let recordName = \"\\(storeIdentifier)_snapshot_manifest\"\n                let recordID = CKRecord.ID(recordName: recordName, zoneID: zoneID ?? .default)\n                let record = CKRecord(recordType: .init(CKRecord.ExchangeType.SnapshotManifest.rawValue), recordID: recordID)\n                record.setExchangeValue(manifestData, forKey: .snapshotManifest)\n                record.setExchangeValue(storeIdentifier, forKey: .storeIdentifier)\n                let operation = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: nil)\n                operation.savePolicy = .allKeys\n                operation.modifyRecordsCompletionBlock = { _, _, error in\n                    if let error = error {\n                        log.error(\"Failed to upload snapshot manifest: \\(error)\")\n                        continuation.resume(throwing: error)\n                    } else {\n                        log.trace(\"Uploaded snapshot manifest\")\n                        continuation.resume()\n                    }\n                }\n                self.database.add(operation)\n            } catch {\n                log.error(\"Failed to encode snapshot manifest: \\(error)\")\n                continuation.resume(throwing: error)\n            }\n        }\n    }\n}\n\n\n// MARK:- Subscriptions\n\npublic extension CloudKitExchange {\n\n    func subscribeForPushNotifications() {\n        log.trace(\"Subscribing for CloudKit push notifications\")\n\n        let info = CKSubscription.NotificationInfo()\n        info.shouldSendContentAvailable = true\n\n        let predicate = NSPredicate(value: true)\n        let subscription = CKQuerySubscription(recordType: .init(CKRecord.ExchangeType.Version.rawValue), predicate: predicate, subscriptionID: CKRecord.ExchangeSubscription.VersionCreated.rawValue, options: CKQuerySubscription.Options.firesOnRecordCreation)\n        subscription.notificationInfo = info\n\n        database.save(subscription) { (_, error) in\n            if let error = error {\n                log.error(\"Error creating subscription: \\(error)\")\n            } else {\n                log.trace(\"Successfully subscribed\")\n            }\n        }\n    }\n\n}\n\n\n// MARK:- Restoration\n\nextension CloudKitExchange {\n\n    public var restorationState: Data? {\n        get {\n            try? JSONEncoder().encode(restoration)\n        }\n        set {\n            if let newValue = newValue, let state = try? JSONDecoder().decode(Restoration.self, from: newValue) {\n                restoration = state\n            }\n        }\n    }\n\n    fileprivate struct Restoration: Codable {\n\n        enum CodingKeys: String, CodingKey {\n            case versionsInCloud, fetchRecordChangesToken, lastQueryDate\n        }\n\n        /// Set of all version ids in cloud\n        var versionsInCloud: Set<Version.ID> = []\n\n        /// Used for private database with custom zone\n        var fetchRecordChangesToken: CKServerChangeToken?\n\n        /// Used when there is no custom zone\n        var lastQueryDate: Date?\n\n        init() {}\n\n        init(from decoder: Decoder) throws {\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n            versionsInCloud = try container.decode(type(of: versionsInCloud), forKey: .versionsInCloud)\n            if let tokenData = try container.decodeIfPresent(Data.self, forKey: .fetchRecordChangesToken) {\n                fetchRecordChangesToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)\n            }\n            lastQueryDate = try container.decodeIfPresent(Date.self, forKey: .lastQueryDate)\n        }\n\n        func encode(to encoder: Encoder) throws {\n            var container = encoder.container(keyedBy: CodingKeys.self)\n            try container.encode(versionsInCloud, forKey: .versionsInCloud)\n            let tokenData = try fetchRecordChangesToken.flatMap {\n                try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false)\n            }\n            try container.encodeIfPresent(tokenData, forKey: .fetchRecordChangesToken)\n            try container.encodeIfPresent(lastQueryDate, forKey: .lastQueryDate)\n        }\n    }\n\n}\n\n\n// MARK:- CKRecord\n\nfileprivate extension CKRecord {\n\n    enum ExchangeSubscription: String {\n        case VersionCreated\n    }\n\n    enum ExchangeType: String {\n        case Version = \"LLVS_Version\"\n        case SnapshotManifest = \"LLVS_SnapshotManifest\"\n        case SnapshotChunk = \"LLVS_SnapshotChunk\"\n    }\n\n    enum ExchangeKey: String {\n        case version, storeIdentifier, valueChanges, valueChangesAsset\n        case snapshotManifest, snapshotChunkIndex, snapshotChunkData\n    }\n\n    func exchangeValue(forKey key: ExchangeKey) -> Any? {\n        return value(forKey: key.rawValue)\n    }\n\n    func setExchangeValue(_ value: Any, forKey key: ExchangeKey) {\n        setValue(value, forKey: key.rawValue)\n    }\n\n}\n"
  },
  {
    "path": "Sources/LLVSGoogleDrive/GoogleDriveAuthenticator.swift",
    "content": "//\n//  GoogleDriveAuthenticator.swift\n//  LLVSGoogleDrive\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n#if canImport(AuthenticationServices)\nimport AuthenticationServices\n#endif\n\n/// Manages OAuth 2.0 authentication for Google Drive.\n///\n/// Handles the full OAuth 2.0 authorization code flow: opening the browser,\n/// exchanging the authorization code for tokens, storing tokens in the Keychain,\n/// and automatically refreshing expired access tokens.\n///\n/// Interactive authorization requires `AuthenticationServices` and is available\n/// on iOS 16+ and macOS 13+.\n///\n/// ```swift\n/// let config = GoogleDriveAuthenticator.Configuration(\n///     clientID: \"your-client-id.apps.googleusercontent.com\",\n///     redirectURI: \"com.yourapp:/oauth2callback\"\n/// )\n/// let authenticator = GoogleDriveAuthenticator(configuration: config)\n///\n/// // First time: interactive authorization\n/// await authenticator.authorize(presenting: window)\n///\n/// // Create file system — tokens refresh automatically\n/// let fs = GoogleDriveFileSystem(authenticator: authenticator)\n/// ```\npublic final class GoogleDriveAuthenticator: @unchecked Sendable {\n\n    // MARK: - Configuration\n\n    public struct Configuration: Sendable {\n        /// The OAuth 2.0 client ID from the Google Cloud Console.\n        public let clientID: String\n\n        /// The redirect URI registered in the Google Cloud Console.\n        public let redirectURI: String\n\n        /// OAuth 2.0 scopes. Defaults to `drive.file` (per-file access).\n        public let scopes: [String]\n\n        public init(\n            clientID: String,\n            redirectURI: String,\n            scopes: [String] = [\"https://www.googleapis.com/auth/drive.file\"]\n        ) {\n            self.clientID = clientID\n            self.redirectURI = redirectURI\n            self.scopes = scopes\n        }\n    }\n\n    // MARK: - Stored Credential\n\n    private struct StoredCredential: Codable {\n        var accessToken: String\n        var refreshToken: String\n        var expiresAt: Date\n    }\n\n    // MARK: - Properties\n\n    public let configuration: Configuration\n\n    private static let authorizationURL = URL(string: \"https://accounts.google.com/o/oauth2/v2/auth\")!\n    private static let tokenURL = URL(string: \"https://oauth2.googleapis.com/token\")!\n\n    private var credential: StoredCredential?\n\n    private var keychainService: String {\n        \"com.llvs.googledrive.\\(configuration.clientID)\"\n    }\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.default\n        config.timeoutIntervalForRequest = 30\n        return URLSession(configuration: config)\n    }()\n\n    // MARK: - Initialization\n\n    public init(configuration: Configuration) {\n        self.configuration = configuration\n        self.credential = loadCredentialFromKeychain()\n    }\n\n    // MARK: - Public API\n\n    /// Whether the user has previously authorized (has a stored refresh token).\n    public var isAuthorized: Bool {\n        credential?.refreshToken != nil\n    }\n\n    /// Returns a valid access token, refreshing if expired.\n    public func validAccessToken() async throws -> String {\n        guard let cred = credential else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        // If token is still valid (with 60-second buffer), return it\n        if cred.expiresAt.timeIntervalSinceNow > 60 {\n            return cred.accessToken\n        }\n\n        return try await refreshAccessToken(refreshToken: cred.refreshToken)\n    }\n\n    /// Clears stored tokens and deauthorizes.\n    public func deauthorize() {\n        credential = nil\n        deleteCredentialFromKeychain()\n    }\n\n    // MARK: - Interactive Authorization\n\n    #if canImport(AuthenticationServices)\n\n    @MainActor\n    public func authorize(presenting anchor: ASPresentationAnchor) async throws {\n        let code = try await obtainAuthorizationCode(presenting: anchor)\n        try await exchangeCodeForTokens(code)\n    }\n\n    @MainActor\n    private func obtainAuthorizationCode(presenting anchor: ASPresentationAnchor) async throws -> String {\n        let scope = configuration.scopes.joined(separator: \" \")\n        var components = URLComponents(url: Self.authorizationURL, resolvingAgainstBaseURL: false)!\n        components.queryItems = [\n            URLQueryItem(name: \"client_id\", value: configuration.clientID),\n            URLQueryItem(name: \"redirect_uri\", value: configuration.redirectURI),\n            URLQueryItem(name: \"response_type\", value: \"code\"),\n            URLQueryItem(name: \"scope\", value: scope),\n            URLQueryItem(name: \"access_type\", value: \"offline\"),\n            URLQueryItem(name: \"prompt\", value: \"consent\"),\n        ]\n\n        let authURL = components.url!\n\n        guard let callbackScheme = URL(string: configuration.redirectURI)?.scheme else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        return try await withCheckedThrowingContinuation { continuation in\n            let anchorProvider = AnchorProvider(anchor: anchor)\n            let session = ASWebAuthenticationSession(\n                url: authURL,\n                callbackURLScheme: callbackScheme\n            ) { callbackURL, error in\n                withExtendedLifetime(anchorProvider) {}\n\n                if let error {\n                    continuation.resume(throwing: error)\n                    return\n                }\n\n                guard let callbackURL,\n                      let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),\n                      let code = components.queryItems?.first(where: { $0.name == \"code\" })?.value else {\n                    continuation.resume(throwing: CloudFileSystemError.authenticationFailed)\n                    return\n                }\n\n                continuation.resume(returning: code)\n            }\n\n            session.presentationContextProvider = anchorProvider\n            session.prefersEphemeralWebBrowserSession = false\n            session.start()\n        }\n    }\n\n    private final class AnchorProvider: NSObject, ASWebAuthenticationPresentationContextProviding {\n        let anchor: ASPresentationAnchor\n\n        init(anchor: ASPresentationAnchor) {\n            self.anchor = anchor\n        }\n\n        func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {\n            anchor\n        }\n    }\n\n    #endif\n\n    // MARK: - Token Exchange\n\n    private func exchangeCodeForTokens(_ code: String) async throws {\n        let body = [\n            \"code\": code,\n            \"client_id\": configuration.clientID,\n            \"redirect_uri\": configuration.redirectURI,\n            \"grant_type\": \"authorization_code\",\n        ]\n\n        let tokenResponse = try await performTokenRequest(body)\n\n        guard let accessToken = tokenResponse[\"access_token\"] as? String,\n              let refreshToken = tokenResponse[\"refresh_token\"] as? String,\n              let expiresIn = tokenResponse[\"expires_in\"] as? Int else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        let cred = StoredCredential(\n            accessToken: accessToken,\n            refreshToken: refreshToken,\n            expiresAt: Date().addingTimeInterval(TimeInterval(expiresIn))\n        )\n        credential = cred\n        saveCredentialToKeychain(cred)\n    }\n\n    private func refreshAccessToken(refreshToken: String) async throws -> String {\n        let body = [\n            \"refresh_token\": refreshToken,\n            \"client_id\": configuration.clientID,\n            \"grant_type\": \"refresh_token\",\n        ]\n\n        let tokenResponse = try await performTokenRequest(body)\n\n        guard let accessToken = tokenResponse[\"access_token\"] as? String,\n              let expiresIn = tokenResponse[\"expires_in\"] as? Int else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        let cred = StoredCredential(\n            accessToken: accessToken,\n            refreshToken: refreshToken,\n            expiresAt: Date().addingTimeInterval(TimeInterval(expiresIn))\n        )\n        credential = cred\n        saveCredentialToKeychain(cred)\n        return accessToken\n    }\n\n    private func performTokenRequest(_ body: [String: String]) async throws -> [String: Any] {\n        var request = URLRequest(url: Self.tokenURL)\n        request.httpMethod = \"POST\"\n        request.setValue(\"application/x-www-form-urlencoded\", forHTTPHeaderField: \"Content-Type\")\n\n        let bodyString = body.map { \"\\($0.key)=\\(urlEncode($0.value))\" }.joined(separator: \"&\")\n        request.httpBody = bodyString.data(using: .utf8)\n\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        return json\n    }\n\n    private func urlEncode(_ string: String) -> String {\n        string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? string\n    }\n\n    // MARK: - Keychain\n\n    private func saveCredentialToKeychain(_ credential: StoredCredential) {\n        guard let data = try? JSONEncoder().encode(credential) else { return }\n\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n        ]\n\n        SecItemDelete(query as CFDictionary)\n\n        var addQuery = query\n        addQuery[kSecValueData as String] = data\n        SecItemAdd(addQuery as CFDictionary, nil)\n    }\n\n    private func loadCredentialFromKeychain() -> StoredCredential? {\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n            kSecReturnData as String: true,\n            kSecMatchLimit as String: kSecMatchLimitOne,\n        ]\n\n        var result: AnyObject?\n        let status = SecItemCopyMatching(query as CFDictionary, &result)\n\n        guard status == errSecSuccess, let data = result as? Data else {\n            return nil\n        }\n\n        return try? JSONDecoder().decode(StoredCredential.self, from: data)\n    }\n\n    private func deleteCredentialFromKeychain() {\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n        ]\n        SecItemDelete(query as CFDictionary)\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSGoogleDrive/GoogleDriveFileSystem.swift",
    "content": "//\n//  GoogleDriveFileSystem.swift\n//  LLVSGoogleDrive\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// A `CloudFileSystem` backed by the Google Drive REST API v3 using `URLSession`.\n///\n/// Google Drive uses file IDs rather than paths to identify files and folders.\n/// This backend bridges the path-based `CloudFileSystem` protocol onto Google Drive's\n/// ID-based API by walking the folder hierarchy to resolve paths to IDs.\n///\n/// Resolved IDs are cached to minimize API calls. The cache is invalidated\n/// when items are deleted.\n///\n/// Two initialization paths are available:\n/// ```swift\n/// // Static access token (app manages refresh externally)\n/// let fs = GoogleDriveFileSystem(accessToken: \"your-token\")\n///\n/// // Authenticator with auto-refresh\n/// let fs = GoogleDriveFileSystem(authenticator: authenticator)\n/// ```\npublic final class GoogleDriveFileSystem: CloudFileSystem, @unchecked Sendable {\n\n    // MARK: - Properties\n\n    private let tokenProvider: @Sendable () async throws -> String\n\n    /// Cache mapping absolute paths to Google Drive folder IDs.\n    private var folderIDCache: [String: String] = [\"/\": \"root\"]\n\n    /// Cache mapping absolute file paths to Google Drive file IDs.\n    private var fileIDCache: [String: String] = [:]\n\n    private static let apiBaseURL = URL(string: \"https://www.googleapis.com/drive/v3/\")!\n    private static let uploadBaseURL = URL(string: \"https://www.googleapis.com/upload/drive/v3/\")!\n    private static let folderMimeType = \"application/vnd.google-apps.folder\"\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.default\n        config.timeoutIntervalForRequest = 60\n        config.timeoutIntervalForResource = 3600\n        return URLSession(configuration: config)\n    }()\n\n    // MARK: - Initialization\n\n    /// Creates a Google Drive file system with a static access token.\n    public init(accessToken: String) {\n        self.tokenProvider = { accessToken }\n    }\n\n    /// Creates a Google Drive file system with an authenticator that\n    /// automatically refreshes expired tokens.\n    public init(authenticator: GoogleDriveAuthenticator) {\n        self.tokenProvider = { try await authenticator.validAccessToken() }\n    }\n\n    // MARK: - CloudFileSystem\n\n    public func fileExists(at path: String) async throws -> Bool {\n        let absPath = absolutePath(for: path)\n        let parentPath = (absPath as NSString).deletingLastPathComponent\n        let name = (absPath as NSString).lastPathComponent\n\n        do {\n            let parentID = try await resolveFolderID(forPath: parentPath)\n\n            // Check for folder\n            let folderQuery = \"'\\(parentID)' in parents and name='\\(escapedQuery(name))' and mimeType='\\(Self.folderMimeType)' and trashed=false\"\n            let folderResult = try await queryFiles(query: folderQuery, fields: \"files(id)\")\n            if !folderResult.isEmpty { return true }\n\n            // Check for file\n            let fileQuery = \"'\\(parentID)' in parents and name='\\(escapedQuery(name))' and trashed=false\"\n            let fileResult = try await queryFiles(query: fileQuery, fields: \"files(id)\")\n            return !fileResult.isEmpty\n        } catch {\n            if isNotFoundError(error) { return false }\n            throw error\n        }\n    }\n\n    public func contentsOfDirectory(at path: String) async throws -> [String] {\n        let absPath = absolutePath(for: path)\n        let folderID: String\n        do {\n            folderID = try await resolveFolderID(forPath: absPath)\n        } catch {\n            if isNotFoundError(error) { throw CloudFileSystemError.fileNotFound }\n            throw error\n        }\n\n        var allNames: [String] = []\n        var pageToken: String? = nil\n\n        repeat {\n            let query = \"'\\(folderID)' in parents and trashed=false\"\n            var params: [String: String] = [\n                \"q\": query,\n                \"fields\": \"nextPageToken,files(id,name,mimeType)\",\n                \"pageSize\": \"1000\"\n            ]\n            if let pageToken { params[\"pageToken\"] = pageToken }\n\n            let token = try await tokenProvider()\n            var request = URLRequest(url: urlWithQuery(Self.apiBaseURL.appendingPathComponent(\"files\"), params: params))\n            request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n            let json = try await performRequest(request)\n            let files = json[\"files\"] as? [[String: Any]] ?? []\n\n            for entry in files {\n                guard let name = entry[\"name\"] as? String else { continue }\n                let mimeType = entry[\"mimeType\"] as? String ?? \"\"\n                let entryPath = (absPath as NSString).appendingPathComponent(name)\n\n                if mimeType == Self.folderMimeType {\n                    if let id = entry[\"id\"] as? String {\n                        folderIDCache[entryPath] = id\n                    }\n                    // Don't include directories in the list\n                } else {\n                    if let id = entry[\"id\"] as? String {\n                        fileIDCache[entryPath] = id\n                    }\n                    allNames.append(name)\n                }\n            }\n\n            pageToken = json[\"nextPageToken\"] as? String\n        } while pageToken != nil\n\n        return allNames\n    }\n\n    public func upload(data: Data, to path: String) async throws {\n        let absPath = absolutePath(for: path)\n        let parentPath = (absPath as NSString).deletingLastPathComponent\n        let fileName = (absPath as NSString).lastPathComponent\n\n        // Create intermediate directories\n        let parentID = try await createIntermediateDirectories(forPath: parentPath)\n\n        // Delete existing file if present\n        if let existingID = fileIDCache[absPath] {\n            try? await deleteItem(withID: existingID)\n            fileIDCache.removeValue(forKey: absPath)\n        } else if let existingID = try? await resolveFileID(forPath: absPath) {\n            try? await deleteItem(withID: existingID)\n            fileIDCache.removeValue(forKey: absPath)\n        }\n\n        // Upload using multipart\n        let metadata: [String: Any] = [\n            \"name\": fileName,\n            \"parents\": [parentID]\n        ]\n        let metadataData = try JSONSerialization.data(withJSONObject: metadata)\n\n        let boundary = UUID().uuidString\n        let token = try await tokenProvider()\n\n        var url = Self.uploadBaseURL.appendingPathComponent(\"files\")\n        url = urlWithQuery(url, params: [\"uploadType\": \"multipart\", \"fields\": \"id\"])\n\n        var request = URLRequest(url: url)\n        request.httpMethod = \"POST\"\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        request.setValue(\"multipart/related; boundary=\\(boundary)\", forHTTPHeaderField: \"Content-Type\")\n        request.timeoutInterval = 3600\n\n        var body = Data()\n        body.append(\"--\\(boundary)\\r\\n\".data(using: .utf8)!)\n        body.append(\"Content-Type: application/json; charset=UTF-8\\r\\n\\r\\n\".data(using: .utf8)!)\n        body.append(metadataData)\n        body.append(\"\\r\\n--\\(boundary)\\r\\n\".data(using: .utf8)!)\n        body.append(\"Content-Type: application/octet-stream\\r\\n\\r\\n\".data(using: .utf8)!)\n        body.append(data)\n        body.append(\"\\r\\n--\\(boundary)--\\r\\n\".data(using: .utf8)!)\n\n        let (responseData, response) = try await session.upload(for: request, from: body)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n\n        let json = try parseJSON(responseData)\n        if let fileID = json[\"id\"] as? String {\n            fileIDCache[absPath] = fileID\n        }\n    }\n\n    public func download(from path: String) async throws -> Data {\n        let absPath = absolutePath(for: path)\n        let fileID = try await resolveFileID(forPath: absPath)\n\n        let token = try await tokenProvider()\n        let url = urlWithQuery(\n            Self.apiBaseURL.appendingPathComponent(\"files/\\(fileID)\"),\n            params: [\"alt\": \"media\"]\n        )\n        var request = URLRequest(url: url)\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        request.timeoutInterval = 3600\n\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n\n        return data\n    }\n\n    public func remove(at path: String) async throws {\n        let absPath = absolutePath(for: path)\n\n        // Try as file\n        if let fileID = try? await resolveFileID(forPath: absPath) {\n            try await deleteItem(withID: fileID)\n            fileIDCache.removeValue(forKey: absPath)\n            return\n        }\n\n        // 404-equivalent: not an error for remove\n    }\n\n    public func removeDirectory(at path: String) async throws {\n        let absPath = absolutePath(for: path)\n\n        // Try as folder\n        var folderID = folderIDCache[absPath]\n        if folderID == nil {\n            folderID = try? await resolveFolderIDFromAPI(forPath: absPath)\n        }\n        if let folderID {\n            try await deleteItem(withID: folderID)\n            // Invalidate cache for this path and any children\n            folderIDCache = folderIDCache.filter { !$0.key.hasPrefix(absPath) || $0.key == \"/\" }\n            fileIDCache = fileIDCache.filter { !$0.key.hasPrefix(absPath) }\n        }\n    }\n\n    // MARK: - Directory Creation\n\n    /// Creates all intermediate directories for the given path, returning the ID of the deepest folder.\n    private func createIntermediateDirectories(forPath path: String) async throws -> String {\n        let absPath = absolutePath(for: path)\n        let components = pathComponents(for: absPath)\n        var currentPath = \"/\"\n        var currentID = \"root\"\n\n        for component in components {\n            let nextPath = (currentPath as NSString).appendingPathComponent(component)\n\n            if let cached = folderIDCache[nextPath] {\n                currentID = cached\n                currentPath = nextPath\n                continue\n            }\n\n            // Check if folder exists\n            let query = \"'\\(currentID)' in parents and name='\\(escapedQuery(component))' and mimeType='\\(Self.folderMimeType)' and trashed=false\"\n            let existing = try await queryFiles(query: query, fields: \"files(id)\")\n\n            if let existingFolder = (existing.first as? [String: Any]),\n               let existingID = existingFolder[\"id\"] as? String {\n                currentID = existingID\n                folderIDCache[nextPath] = currentID\n                currentPath = nextPath\n                continue\n            }\n\n            // Create the folder\n            let metadata: [String: Any] = [\n                \"name\": component,\n                \"mimeType\": Self.folderMimeType,\n                \"parents\": [currentID]\n            ]\n\n            let token = try await tokenProvider()\n            var request = URLRequest(url: Self.apiBaseURL.appendingPathComponent(\"files\"))\n            request.httpMethod = \"POST\"\n            request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n            request.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n            request.httpBody = try JSONSerialization.data(withJSONObject: metadata)\n\n            let json = try await performRequest(request)\n            if let folderID = json[\"id\"] as? String {\n                currentID = folderID\n                folderIDCache[nextPath] = currentID\n            }\n            currentPath = nextPath\n        }\n\n        return currentID\n    }\n\n    // MARK: - Path Resolution\n\n    private func resolveFolderID(forPath path: String) async throws -> String {\n        let absPath = absolutePath(for: path)\n        if let cached = folderIDCache[absPath] { return cached }\n\n        let components = pathComponents(for: absPath)\n        var currentPath = \"/\"\n        var currentID = \"root\"\n\n        for component in components {\n            let nextPath = (currentPath as NSString).appendingPathComponent(component)\n\n            if let cached = folderIDCache[nextPath] {\n                currentID = cached\n                currentPath = nextPath\n                continue\n            }\n\n            let query = \"'\\(currentID)' in parents and name='\\(escapedQuery(component))' and mimeType='\\(Self.folderMimeType)' and trashed=false\"\n            let results = try await queryFiles(query: query, fields: \"files(id,name)\")\n\n            guard let folder = results.first as? [String: Any],\n                  let folderID = folder[\"id\"] as? String else {\n                throw CloudFileSystemError.fileNotFound\n            }\n\n            currentID = folderID\n            folderIDCache[nextPath] = currentID\n            currentPath = nextPath\n        }\n\n        return currentID\n    }\n\n    private func resolveFolderIDFromAPI(forPath path: String) async throws -> String {\n        let absPath = absolutePath(for: path)\n        let parentPath = (absPath as NSString).deletingLastPathComponent\n        let name = (absPath as NSString).lastPathComponent\n\n        let parentID = try await resolveFolderID(forPath: parentPath)\n        let query = \"'\\(parentID)' in parents and name='\\(escapedQuery(name))' and mimeType='\\(Self.folderMimeType)' and trashed=false\"\n        let results = try await queryFiles(query: query, fields: \"files(id)\")\n\n        guard let folder = results.first as? [String: Any],\n              let folderID = folder[\"id\"] as? String else {\n            throw CloudFileSystemError.fileNotFound\n        }\n\n        folderIDCache[absPath] = folderID\n        return folderID\n    }\n\n    private func resolveFileID(forPath path: String) async throws -> String {\n        let absPath = absolutePath(for: path)\n        if let cached = fileIDCache[absPath] { return cached }\n\n        let parentPath = (absPath as NSString).deletingLastPathComponent\n        let fileName = (absPath as NSString).lastPathComponent\n\n        let parentID = try await resolveFolderID(forPath: parentPath)\n        let query = \"'\\(parentID)' in parents and name='\\(escapedQuery(fileName))' and trashed=false\"\n        let results = try await queryFiles(query: query, fields: \"files(id,name)\")\n\n        guard let file = results.first as? [String: Any],\n              let fileID = file[\"id\"] as? String else {\n            throw CloudFileSystemError.fileNotFound\n        }\n\n        fileIDCache[absPath] = fileID\n        return fileID\n    }\n\n    // MARK: - API Helpers\n\n    private func queryFiles(query: String, fields: String) async throws -> [Any] {\n        let token = try await tokenProvider()\n        let url = urlWithQuery(Self.apiBaseURL.appendingPathComponent(\"files\"), params: [\n            \"q\": query,\n            \"fields\": fields\n        ])\n        var request = URLRequest(url: url)\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n        let json = try await performRequest(request)\n        return json[\"files\"] as? [Any] ?? []\n    }\n\n    private func deleteItem(withID itemID: String) async throws {\n        let token = try await tokenProvider()\n        var request = URLRequest(url: Self.apiBaseURL.appendingPathComponent(\"files/\\(itemID)\"))\n        request.httpMethod = \"DELETE\"\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        // 204 No Content = success, 404 = already deleted\n        guard statusCode == 204 || statusCode == 404 else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n    }\n\n    private func performRequest(_ request: URLRequest) async throws -> [String: Any] {\n        var req = request\n        req.cachePolicy = .reloadIgnoringLocalCacheData\n        if req.timeoutInterval == 0 { req.timeoutInterval = 60 }\n\n        let (data, response) = try await session.data(for: req)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n\n        return try parseJSON(data)\n    }\n\n    private func parseJSON(_ data: Data) throws -> [String: Any] {\n        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {\n            throw CloudFileSystemError.serverError(statusCode: 0)\n        }\n        return json\n    }\n\n    // MARK: - Path Helpers\n\n    func absolutePath(for path: String) -> String {\n        var absPath = path\n        if !absPath.hasPrefix(\"/\") { absPath = \"/\" + absPath }\n        while absPath.contains(\"//\") {\n            absPath = absPath.replacingOccurrences(of: \"//\", with: \"/\")\n        }\n        if absPath != \"/\", absPath.hasSuffix(\"/\") {\n            absPath = String(absPath.dropLast())\n        }\n        return absPath\n    }\n\n    func pathComponents(for path: String) -> [String] {\n        absolutePath(for: path).split(separator: \"/\").map(String.init)\n    }\n\n    private func escapedQuery(_ value: String) -> String {\n        value.replacingOccurrences(of: \"'\", with: \"\\\\'\")\n    }\n\n    private func urlWithQuery(_ url: URL, params: [String: String]) -> URL {\n        var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!\n        components.queryItems = params.map { URLQueryItem(name: $0.key, value: $0.value) }\n        return components.url!\n    }\n\n    // MARK: - Error Handling\n\n    private func isNotFoundError(_ error: any Error) -> Bool {\n        if let cfsError = error as? CloudFileSystemError {\n            if case .fileNotFound = cfsError { return true }\n        }\n        return false\n    }\n\n    func mapHTTPError(statusCode: Int) -> CloudFileSystemError {\n        switch statusCode {\n        case 401: return .authenticationFailed\n        case 404: return .fileNotFound\n        default: return .serverError(statusCode: statusCode)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSModel/Macros.swift",
    "content": "//\n//  Macros.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\n/// Generates a `Mergeable` conformance with property-wise 3-way merge.\n///\n/// Apply to a struct to automatically synthesize `merged(withSubordinate:commonAncestor:)`\n/// and `salvaging(from:)`. All stored `var` properties are merged individually;\n/// computed properties, static properties, and `let` constants are skipped.\n@attached(extension, conformances: Mergeable, names: arbitrary)\npublic macro MergeableModel() = #externalMacro(module: \"LLVSModelMacros\", type: \"MergeableModelMacro\")\n"
  },
  {
    "path": "Sources/LLVSModel/Mergeable.swift",
    "content": "//\n//  Mergeable.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport Foundation\n\n/// A type that supports property-wise 3-way merge.\n///\n/// Conform to this protocol (typically via the `@MergeableModel` macro) to enable\n/// automatic conflict resolution in `MergeableArbiter`.\npublic protocol Mergeable: Equatable {\n    /// Merge `self` (dominant) with `other` (subordinate), using `commonAncestor`\n    /// to determine which properties changed on each branch.\n    func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self\n\n    /// Resolve a conflict when no common ancestor is available (e.g. twice-inserted).\n    /// Default: returns `self` (dominant wins).\n    func salvaging(from other: Self) throws -> Self\n}\n\npublic extension Mergeable {\n    func salvaging(from other: Self) throws -> Self {\n        self\n    }\n}\n\n// MARK: - Property Merge Helpers\n\npublic func mergeProperty<T: Equatable>(_ dominant: T, _ subordinate: T, _ ancestor: T) throws -> T {\n    dominant == ancestor ? subordinate : dominant\n}\n\npublic func mergeProperty<T: Mergeable>(_ dominant: T, _ subordinate: T, _ ancestor: T) throws -> T {\n    try dominant.merged(withSubordinate: subordinate, commonAncestor: ancestor)\n}\n\npublic func salvageProperty<T: Equatable>(_ dominant: T, _ subordinate: T) throws -> T {\n    dominant\n}\n\npublic func salvageProperty<T: Mergeable>(_ dominant: T, _ subordinate: T) throws -> T {\n    try dominant.salvaging(from: subordinate)\n}\n\n// MARK: - Optional Mergeable\n\nextension Optional: Mergeable where Wrapped: Mergeable {\n    public func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {\n        switch (self, other, commonAncestor) {\n        case let (.some(d), .some(s), .some(a)):\n            return try .some(d.merged(withSubordinate: s, commonAncestor: a))\n        case (.some(let d), .none, .some(let a)):\n            return d == a ? other : self\n        case (.none, _, .some):\n            return self\n        case (_, _, .none):\n            return self\n        }\n    }\n\n    public func salvaging(from other: Self) throws -> Self {\n        switch (self, other) {\n        case let (.some(d), .some(s)):\n            return try .some(d.salvaging(from: s))\n        default:\n            return self\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSModel/MergeableArbiter.swift",
    "content": "//\n//  MergeableArbiter.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// A `MergeArbiter` that bridges LLVS merge conflicts to\n/// `Mergeable.merged(withSubordinate:commonAncestor:)` for property-wise resolution.\n///\n/// Register each `StorableModel & Mergeable` type before merging. Unregistered\n/// value types fall through to `fallbackArbiter`.\npublic class MergeableArbiter: MergeArbiter {\n\n    /// Type-erased merge closure: `(dominant, subordinate, ancestor?) -> merged`\n    private var registry: [String: (Data, Data, Data?) throws -> Data] = [:]\n\n    /// Arbiter used for values whose `Value.ID` has no registered type prefix.\n    public var fallbackArbiter: MergeArbiter\n\n    public init(fallbackArbiter: MergeArbiter = MostRecentChangeFavoringArbiter()) {\n        self.fallbackArbiter = fallbackArbiter\n    }\n\n    /// Register a `StorableModel & Mergeable` type so its conflicts are resolved\n    /// with property-wise 3-way merge.\n    public func register<T: StorableModel & Mergeable>(_ type: T.Type) {\n        let encoder = JSONEncoder()\n        let decoder = JSONDecoder()\n        registry[T.modelTypeIdentifier] = { dominantData, subordinateData, ancestorData in\n            let dominant = try decoder.decode(T.self, from: dominantData)\n            let subordinate = try decoder.decode(T.self, from: subordinateData)\n            let merged: T\n            if let ancestorData {\n                let ancestor = try decoder.decode(T.self, from: ancestorData)\n                merged = try dominant.merged(withSubordinate: subordinate, commonAncestor: ancestor)\n            } else {\n                merged = try dominant.salvaging(from: subordinate)\n            }\n            return try encoder.encode(merged)\n        }\n    }\n\n    // MARK: - MergeArbiter\n\n    public func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change] {\n        // Split forks into registered (model) and unregistered (fallback)\n        var modelForks: [Value.ID: Value.Fork] = [:]\n        var fallbackForks: [Value.ID: Value.Fork] = [:]\n\n        for (valueId, fork) in merge.forksByValueIdentifier {\n            guard fork.isConflicting else { continue }\n            if let typeId = modelTypeIdentifier(from: valueId), registry[typeId] != nil {\n                modelForks[valueId] = fork\n            } else {\n                fallbackForks[valueId] = fork\n            }\n        }\n\n        // Resolve model forks with Mergeable\n        var changes: [Value.Change] = []\n        let v = merge.versions\n\n        for (valueId, fork) in modelForks {\n            switch fork {\n            case .twiceUpdated:\n                let firstValue = try store.value(id: valueId, at: v.first.id)!\n                let secondValue = try store.value(id: valueId, at: v.second.id)!\n                var ancestorData: Data? = nil\n                if let ancestor = merge.commonAncestor {\n                    ancestorData = try store.value(id: valueId, at: ancestor.id)?.data\n                }\n                let typeId = modelTypeIdentifier(from: valueId)!\n                let mergedData = try registry[typeId]!(firstValue.data, secondValue.data, ancestorData)\n                changes.append(.update(Value(id: valueId, data: mergedData)))\n\n            case .twiceInserted:\n                let firstValue = try store.value(id: valueId, at: v.first.id)!\n                let secondValue = try store.value(id: valueId, at: v.second.id)!\n                let typeId = modelTypeIdentifier(from: valueId)!\n                // No ancestor for twice-inserted\n                let mergedData = try registry[typeId]!(firstValue.data, secondValue.data, nil)\n                changes.append(.update(Value(id: valueId, data: mergedData)))\n\n            case .removedAndUpdated(let removedOn):\n                // Favor the update (preserve the non-removed branch's value)\n                let updatedVersion = removedOn == .first ? v.second : v.first\n                let value = try store.value(id: valueId, at: updatedVersion.id)!\n                changes.append(.preserve(value.reference!))\n\n            default:\n                break\n            }\n        }\n\n        // Resolve fallback forks\n        if !fallbackForks.isEmpty {\n            var fallbackMerge = merge\n            fallbackMerge.forksByValueIdentifier = fallbackForks\n            let fallbackChanges = try fallbackArbiter.changes(toResolve: fallbackMerge, in: store)\n            changes.append(contentsOf: fallbackChanges)\n        }\n\n        return changes\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSModel/StorableModel.swift",
    "content": "//\n//  StorableModel.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// A type that can be stored as a typed model value in LLVS.\n///\n/// Conforming types get a stable `modelTypeIdentifier` (e.g. `\"Contact\"`)\n/// used as a prefix in the LLVS `Value.ID`: `\"Contact/uuid-string\"`.\n///\n/// Does not require `Mergeable` — the `@MergeableModel` macro adds that\n/// conformance separately.\npublic protocol StorableModel: Codable {\n    static var modelTypeIdentifier: String { get }\n}\n\n// MARK: - Value.ID Helpers\n\n/// Builds an LLVS `Value.ID` from a type identifier and instance identifier.\n/// The format is `\"TypeName/instance-id\"`.\npublic func modelValueID(typeIdentifier: String, instanceIdentifier: String) -> Value.ID {\n    Value.ID(\"\\(typeIdentifier)/\\(instanceIdentifier)\")\n}\n\n/// Extracts the model type identifier (prefix before the first `/`) from a `Value.ID`.\n/// Returns `nil` if the ID contains no `/`.\npublic func modelTypeIdentifier(from valueID: Value.ID) -> String? {\n    guard let slashIndex = valueID.rawValue.firstIndex(of: \"/\") else { return nil }\n    return String(valueID.rawValue[..<slashIndex])\n}\n\n/// Extracts the instance identifier (suffix after the first `/`) from a `Value.ID`.\n/// Returns `nil` if the ID contains no `/`.\npublic func instanceIdentifier(from valueID: Value.ID) -> String? {\n    guard let slashIndex = valueID.rawValue.firstIndex(of: \"/\") else { return nil }\n    return String(valueID.rawValue[valueID.rawValue.index(after: slashIndex)...])\n}\n"
  },
  {
    "path": "Sources/LLVSModel/StoreCoordinator+Model.swift",
    "content": "//\n//  StoreCoordinator+Model.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\npublic extension StoreCoordinator {\n\n    /// Save a model value, inserting or updating as appropriate.\n    func save<T: StorableModel>(_ model: T, instanceIdentifier: String, in branch: Branch? = nil) throws {\n        let valueId = modelValueID(typeIdentifier: T.modelTypeIdentifier, instanceIdentifier: instanceIdentifier)\n        let data = try JSONEncoder().encode(model)\n        let existing = try value(id: valueId, on: branch)\n        if existing != nil {\n            try save(updating: [Value(id: valueId, data: data)], in: branch)\n        } else {\n            try save(inserting: [Value(id: valueId, data: data)], in: branch)\n        }\n    }\n\n    /// Fetch a single model value by its instance identifier.\n    func fetchModel<T: StorableModel>(_ type: T.Type, instanceIdentifier: String, on branch: Branch? = nil) throws -> T? {\n        let valueId = modelValueID(typeIdentifier: T.modelTypeIdentifier, instanceIdentifier: instanceIdentifier)\n        guard let value = try value(id: valueId, on: branch) else { return nil }\n        return try JSONDecoder().decode(T.self, from: value.data)\n    }\n\n    /// Remove a model value.\n    func removeModel<T: StorableModel>(_ type: T.Type, instanceIdentifier: String, in branch: Branch? = nil) throws {\n        let valueId = modelValueID(typeIdentifier: T.modelTypeIdentifier, instanceIdentifier: instanceIdentifier)\n        try save(removing: [valueId], in: branch)\n    }\n\n    /// Fetch all model values of a given type by scanning value references for matching prefixes.\n    func fetchAllModels<T: StorableModel>(_ type: T.Type, at version: Version.ID? = nil) throws -> [T] {\n        let prefix = T.modelTypeIdentifier + \"/\"\n        let refs = try valueReferences(at: version)\n        let decoder = JSONDecoder()\n        return try refs.compactMap { ref in\n            guard ref.valueId.rawValue.hasPrefix(prefix) else { return nil }\n            guard let value = try store.value(storedAt: ref) else { return nil }\n            return try decoder.decode(T.self, from: value.data)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSModelMacros/MergeableModelMacro.swift",
    "content": "//\n//  MergeableModelMacro.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftSyntax\nimport SwiftSyntaxMacros\n\nenum MergeableModelMacroError: Error, CustomStringConvertible {\n    case onlyApplicableToStruct\n\n    var description: String {\n        switch self {\n        case .onlyApplicableToStruct:\n            return \"@MergeableModel can only be applied to a struct\"\n        }\n    }\n}\n\npublic struct MergeableModelMacro: ExtensionMacro {\n    public static func expansion(\n        of node: AttributeSyntax,\n        attachedTo declaration: some DeclGroupSyntax,\n        providingExtensionsOf type: some TypeSyntaxProtocol,\n        conformingTo protocols: [TypeSyntax],\n        in context: some MacroExpansionContext\n    ) throws -> [ExtensionDeclSyntax] {\n        guard declaration.is(StructDeclSyntax.self) else {\n            throw MergeableModelMacroError.onlyApplicableToStruct\n        }\n\n        let storedProperties = declaration.memberBlock.members.compactMap { member -> String? in\n            guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { return nil }\n\n            // Skip `let` constants\n            guard varDecl.bindingSpecifier.tokenKind == .keyword(.var) else { return nil }\n\n            // Skip static/class properties\n            let isStatic = varDecl.modifiers.contains { modifier in\n                modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)\n            }\n            guard !isStatic else { return nil }\n\n            guard let binding = varDecl.bindings.first,\n                  let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {\n                return nil\n            }\n\n            // Skip computed properties\n            if let accessorBlock = binding.accessorBlock {\n                switch accessorBlock.accessors {\n                case .getter:\n                    // Shorthand computed property: `var x: T { ... }`\n                    return nil\n                case .accessors(let accessorList):\n                    let isComputed = accessorList.contains { accessor in\n                        accessor.accessorSpecifier.tokenKind == .keyword(.get) ||\n                        accessor.accessorSpecifier.tokenKind == .keyword(.set)\n                    }\n                    if isComputed { return nil }\n                }\n            }\n\n            return pattern.identifier.text\n        }\n\n        var mergedStatements = [\"var result = self\"]\n        for prop in storedProperties {\n            mergedStatements.append(\"result.\\(prop) = try mergeProperty(self.\\(prop), other.\\(prop), commonAncestor.\\(prop))\")\n        }\n        mergedStatements.append(\"return result\")\n        let mergedBody = mergedStatements.joined(separator: \"\\n        \")\n\n        var salvagingStatements = [\"var result = self\"]\n        for prop in storedProperties {\n            salvagingStatements.append(\"result.\\(prop) = try salvageProperty(self.\\(prop), other.\\(prop))\")\n        }\n        salvagingStatements.append(\"return result\")\n        let salvagingBody = salvagingStatements.joined(separator: \"\\n        \")\n\n        let extensionDecl: DeclSyntax = \"\"\"\n        extension \\(type.trimmed): LLVSModel.Mergeable {\n            func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {\n                \\(raw: mergedBody)\n            }\n            func salvaging(from other: Self) throws -> Self {\n                \\(raw: salvagingBody)\n            }\n        }\n        \"\"\"\n\n        return [extensionDecl.cast(ExtensionDeclSyntax.self)]\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSModelMacros/Plugin.swift",
    "content": "//\n//  Plugin.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftCompilerPlugin\nimport SwiftSyntaxMacros\n\n@main\nstruct LLVSModelMacroPlugin: CompilerPlugin {\n    var providingMacros: [Macro.Type] = [\n        MergeableModelMacro.self,\n    ]\n}\n"
  },
  {
    "path": "Sources/LLVSOneDrive/OneDriveAuthenticator.swift",
    "content": "//\n//  OneDriveAuthenticator.swift\n//  LLVSOneDrive\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n#if canImport(AuthenticationServices)\nimport AuthenticationServices\n#endif\n\n/// Manages OAuth 2.0 authentication for Microsoft OneDrive via Microsoft Identity Platform.\n///\n/// Handles the full OAuth 2.0 authorization code flow: opening the browser,\n/// exchanging the authorization code for tokens, storing tokens in the Keychain,\n/// and automatically refreshing expired access tokens.\n///\n/// Interactive authorization requires `AuthenticationServices` and is available\n/// on iOS 16+ and macOS 13+.\n///\n/// ```swift\n/// let config = OneDriveAuthenticator.Configuration(\n///     clientID: \"your-app-client-id\",\n///     redirectURI: \"msauth.com.yourapp://auth\"\n/// )\n/// let authenticator = OneDriveAuthenticator(configuration: config)\n///\n/// // First time: interactive authorization\n/// await authenticator.authorize(presenting: window)\n///\n/// // Create file system — tokens refresh automatically\n/// let fs = OneDriveFileSystem(authenticator: authenticator)\n/// ```\npublic final class OneDriveAuthenticator: @unchecked Sendable {\n\n    // MARK: - Configuration\n\n    public struct Configuration: Sendable {\n        /// The Application (client) ID from Azure App Registration.\n        public let clientID: String\n\n        /// The redirect URI registered in Azure App Registration.\n        public let redirectURI: String\n\n        /// OAuth 2.0 scopes. Defaults to `Files.ReadWrite` and `offline_access`.\n        public let scopes: [String]\n\n        /// The Azure AD tenant. Defaults to `common` (personal + work accounts).\n        public let tenant: String\n\n        public init(\n            clientID: String,\n            redirectURI: String,\n            scopes: [String] = [\"Files.ReadWrite\", \"offline_access\"],\n            tenant: String = \"common\"\n        ) {\n            self.clientID = clientID\n            self.redirectURI = redirectURI\n            self.scopes = scopes\n            self.tenant = tenant\n        }\n    }\n\n    // MARK: - Stored Credential\n\n    private struct StoredCredential: Codable {\n        var accessToken: String\n        var refreshToken: String\n        var expiresAt: Date\n    }\n\n    // MARK: - Properties\n\n    public let configuration: Configuration\n\n    private var authorizationURL: URL {\n        URL(string: \"https://login.microsoftonline.com/\\(configuration.tenant)/oauth2/v2.0/authorize\")!\n    }\n\n    private var tokenURL: URL {\n        URL(string: \"https://login.microsoftonline.com/\\(configuration.tenant)/oauth2/v2.0/token\")!\n    }\n\n    private var credential: StoredCredential?\n\n    private var keychainService: String {\n        \"com.llvs.onedrive.\\(configuration.clientID)\"\n    }\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.default\n        config.timeoutIntervalForRequest = 30\n        return URLSession(configuration: config)\n    }()\n\n    // MARK: - Initialization\n\n    public init(configuration: Configuration) {\n        self.configuration = configuration\n        self.credential = loadCredentialFromKeychain()\n    }\n\n    // MARK: - Public API\n\n    /// Whether the user has previously authorized (has a stored refresh token).\n    public var isAuthorized: Bool {\n        credential?.refreshToken != nil\n    }\n\n    /// Returns a valid access token, refreshing if expired.\n    public func validAccessToken() async throws -> String {\n        guard let cred = credential else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        // If token is still valid (with 60-second buffer), return it\n        if cred.expiresAt.timeIntervalSinceNow > 60 {\n            return cred.accessToken\n        }\n\n        return try await refreshAccessToken(refreshToken: cred.refreshToken)\n    }\n\n    /// Clears stored tokens and deauthorizes.\n    public func deauthorize() {\n        credential = nil\n        deleteCredentialFromKeychain()\n    }\n\n    // MARK: - Interactive Authorization\n\n    #if canImport(AuthenticationServices)\n\n    @MainActor\n    public func authorize(presenting anchor: ASPresentationAnchor) async throws {\n        let code = try await obtainAuthorizationCode(presenting: anchor)\n        try await exchangeCodeForTokens(code)\n    }\n\n    @MainActor\n    private func obtainAuthorizationCode(presenting anchor: ASPresentationAnchor) async throws -> String {\n        let scope = configuration.scopes.joined(separator: \" \")\n        var components = URLComponents(url: authorizationURL, resolvingAgainstBaseURL: false)!\n        components.queryItems = [\n            URLQueryItem(name: \"client_id\", value: configuration.clientID),\n            URLQueryItem(name: \"redirect_uri\", value: configuration.redirectURI),\n            URLQueryItem(name: \"response_type\", value: \"code\"),\n            URLQueryItem(name: \"scope\", value: scope),\n            URLQueryItem(name: \"prompt\", value: \"consent\"),\n        ]\n\n        let authURL = components.url!\n\n        guard let callbackScheme = URL(string: configuration.redirectURI)?.scheme else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        return try await withCheckedThrowingContinuation { continuation in\n            let anchorProvider = AnchorProvider(anchor: anchor)\n            let session = ASWebAuthenticationSession(\n                url: authURL,\n                callbackURLScheme: callbackScheme\n            ) { callbackURL, error in\n                withExtendedLifetime(anchorProvider) {}\n\n                if let error {\n                    continuation.resume(throwing: error)\n                    return\n                }\n\n                guard let callbackURL,\n                      let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),\n                      let code = components.queryItems?.first(where: { $0.name == \"code\" })?.value else {\n                    continuation.resume(throwing: CloudFileSystemError.authenticationFailed)\n                    return\n                }\n\n                continuation.resume(returning: code)\n            }\n\n            session.presentationContextProvider = anchorProvider\n            session.prefersEphemeralWebBrowserSession = false\n            session.start()\n        }\n    }\n\n    private final class AnchorProvider: NSObject, ASWebAuthenticationPresentationContextProviding {\n        let anchor: ASPresentationAnchor\n\n        init(anchor: ASPresentationAnchor) {\n            self.anchor = anchor\n        }\n\n        func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {\n            anchor\n        }\n    }\n\n    #endif\n\n    // MARK: - Token Exchange\n\n    private func exchangeCodeForTokens(_ code: String) async throws {\n        let body = [\n            \"code\": code,\n            \"client_id\": configuration.clientID,\n            \"redirect_uri\": configuration.redirectURI,\n            \"grant_type\": \"authorization_code\",\n            \"scope\": configuration.scopes.joined(separator: \" \"),\n        ]\n\n        let tokenResponse = try await performTokenRequest(body)\n\n        guard let accessToken = tokenResponse[\"access_token\"] as? String,\n              let refreshToken = tokenResponse[\"refresh_token\"] as? String,\n              let expiresIn = tokenResponse[\"expires_in\"] as? Int else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        let cred = StoredCredential(\n            accessToken: accessToken,\n            refreshToken: refreshToken,\n            expiresAt: Date().addingTimeInterval(TimeInterval(expiresIn))\n        )\n        credential = cred\n        saveCredentialToKeychain(cred)\n    }\n\n    private func refreshAccessToken(refreshToken: String) async throws -> String {\n        let body = [\n            \"refresh_token\": refreshToken,\n            \"client_id\": configuration.clientID,\n            \"grant_type\": \"refresh_token\",\n            \"scope\": configuration.scopes.joined(separator: \" \"),\n        ]\n\n        let tokenResponse = try await performTokenRequest(body)\n\n        guard let accessToken = tokenResponse[\"access_token\"] as? String,\n              let expiresIn = tokenResponse[\"expires_in\"] as? Int else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        // Microsoft may return a new refresh token — use it if present\n        let newRefreshToken = tokenResponse[\"refresh_token\"] as? String ?? refreshToken\n\n        let cred = StoredCredential(\n            accessToken: accessToken,\n            refreshToken: newRefreshToken,\n            expiresAt: Date().addingTimeInterval(TimeInterval(expiresIn))\n        )\n        credential = cred\n        saveCredentialToKeychain(cred)\n        return accessToken\n    }\n\n    private func performTokenRequest(_ body: [String: String]) async throws -> [String: Any] {\n        var request = URLRequest(url: tokenURL)\n        request.httpMethod = \"POST\"\n        request.setValue(\"application/x-www-form-urlencoded\", forHTTPHeaderField: \"Content-Type\")\n\n        let bodyString = body.map { \"\\($0.key)=\\(urlEncode($0.value))\" }.joined(separator: \"&\")\n        request.httpBody = bodyString.data(using: .utf8)\n\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {\n            throw CloudFileSystemError.authenticationFailed\n        }\n\n        return json\n    }\n\n    private func urlEncode(_ string: String) -> String {\n        string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? string\n    }\n\n    // MARK: - Keychain\n\n    private func saveCredentialToKeychain(_ credential: StoredCredential) {\n        guard let data = try? JSONEncoder().encode(credential) else { return }\n\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n        ]\n\n        SecItemDelete(query as CFDictionary)\n\n        var addQuery = query\n        addQuery[kSecValueData as String] = data\n        SecItemAdd(addQuery as CFDictionary, nil)\n    }\n\n    private func loadCredentialFromKeychain() -> StoredCredential? {\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n            kSecReturnData as String: true,\n            kSecMatchLimit as String: kSecMatchLimitOne,\n        ]\n\n        var result: AnyObject?\n        let status = SecItemCopyMatching(query as CFDictionary, &result)\n\n        guard status == errSecSuccess, let data = result as? Data else {\n            return nil\n        }\n\n        return try? JSONDecoder().decode(StoredCredential.self, from: data)\n    }\n\n    private func deleteCredentialFromKeychain() {\n        let query: [String: Any] = [\n            kSecClass as String: kSecClassGenericPassword,\n            kSecAttrService as String: keychainService,\n            kSecAttrAccount as String: \"credential\",\n        ]\n        SecItemDelete(query as CFDictionary)\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSOneDrive/OneDriveFileSystem.swift",
    "content": "//\n//  OneDriveFileSystem.swift\n//  LLVSOneDrive\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// A `CloudFileSystem` backed by the Microsoft Graph REST API v1.0 using `URLSession`.\n///\n/// Microsoft Graph supports native path-based addressing using the colon syntax:\n/// `/me/drive/root:/path/to/item:`. This means paths map directly to OneDrive paths\n/// without any ID resolution or caching.\n///\n/// Two initialization paths are available:\n/// ```swift\n/// // Static access token (app manages refresh externally)\n/// let fs = OneDriveFileSystem(accessToken: \"your-token\")\n///\n/// // Authenticator with auto-refresh\n/// let fs = OneDriveFileSystem(authenticator: authenticator)\n/// ```\npublic final class OneDriveFileSystem: CloudFileSystem, @unchecked Sendable {\n\n    // MARK: - Properties\n\n    private let tokenProvider: @Sendable () async throws -> String\n\n    private static let graphBaseURL = URL(string: \"https://graph.microsoft.com/v1.0\")!\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.default\n        config.timeoutIntervalForRequest = 60\n        config.timeoutIntervalForResource = 3600\n        return URLSession(configuration: config)\n    }()\n\n    // MARK: - Initialization\n\n    /// Creates a OneDrive file system with a static access token.\n    public init(accessToken: String) {\n        self.tokenProvider = { accessToken }\n    }\n\n    /// Creates a OneDrive file system with an authenticator that\n    /// automatically refreshes expired tokens.\n    public init(authenticator: OneDriveAuthenticator) {\n        self.tokenProvider = { try await authenticator.validAccessToken() }\n    }\n\n    // MARK: - CloudFileSystem\n\n    public func fileExists(at path: String) async throws -> Bool {\n        let token = try await tokenProvider()\n        let url = graphURL(forItemAtPath: path)\n        var request = URLRequest(url: url)\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        if statusCode == 404 { return false }\n        if (200..<300).contains(statusCode) { return true }\n        throw mapHTTPError(statusCode: statusCode)\n    }\n\n    public func contentsOfDirectory(at path: String) async throws -> [String] {\n        let absPath = absolutePath(for: path)\n        var allNames: [String] = []\n        var nextURL: URL? = graphURL(forChildrenAtPath: absPath)\n\n        while let currentURL = nextURL {\n            let token = try await tokenProvider()\n            var request = URLRequest(url: currentURL)\n            request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n            let json = try await performRequest(request)\n            let entries = json[\"value\"] as? [[String: Any]] ?? []\n\n            for entry in entries {\n                guard let name = entry[\"name\"] as? String else { continue }\n                // Skip folders, return only files\n                if entry[\"folder\"] == nil {\n                    allNames.append(name)\n                }\n            }\n\n            // Handle pagination\n            if let nextLink = json[\"@odata.nextLink\"] as? String,\n               let url = URL(string: nextLink) {\n                nextURL = url\n            } else {\n                nextURL = nil\n            }\n        }\n\n        return allNames\n    }\n\n    public func upload(data: Data, to path: String) async throws {\n        // OneDrive auto-creates intermediate folders on PUT\n        let token = try await tokenProvider()\n        let url = graphURL(forContentAtPath: path)\n\n        var request = URLRequest(url: url)\n        request.httpMethod = \"PUT\"\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        request.setValue(\"application/octet-stream\", forHTTPHeaderField: \"Content-Type\")\n        request.timeoutInterval = 3600\n\n        let (_, response) = try await session.upload(for: request, from: data)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n    }\n\n    public func download(from path: String) async throws -> Data {\n        let token = try await tokenProvider()\n        let url = graphURL(forContentAtPath: path)\n\n        var request = URLRequest(url: url)\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n        request.timeoutInterval = 3600\n\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        if statusCode == 404 {\n            throw CloudFileSystemError.fileNotFound\n        }\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n\n        return data\n    }\n\n    public func remove(at path: String) async throws {\n        let token = try await tokenProvider()\n        let url = graphURL(forItemAtPath: path)\n        var request = URLRequest(url: url)\n        request.httpMethod = \"DELETE\"\n        request.setValue(\"Bearer \\(token)\", forHTTPHeaderField: \"Authorization\")\n\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        // 204 No Content = success, 404 = already gone\n        guard statusCode == 204 || statusCode == 404 else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n    }\n\n    public func removeDirectory(at path: String) async throws {\n        // OneDrive DELETE on a folder removes it recursively\n        try await remove(at: path)\n    }\n\n    // MARK: - URL Construction\n\n    /// Returns the Graph API URL for a drive item at the given path.\n    /// Uses the colon syntax: `/me/drive/root:/path/to/item:`\n    func graphURL(forItemAtPath path: String) -> URL {\n        let absPath = absolutePath(for: path)\n        if absPath == \"/\" {\n            return Self.graphBaseURL.appendingPathComponent(\"me/drive/root\")\n        }\n        let encoded = encodePathForGraph(absPath)\n        let urlString = \"\\(Self.graphBaseURL.absoluteString)/me/drive/root:\\(encoded):\"\n        return URL(string: urlString)!\n    }\n\n    /// Returns the Graph API URL for listing children of a directory.\n    func graphURL(forChildrenAtPath path: String) -> URL {\n        let absPath = absolutePath(for: path)\n        if absPath == \"/\" {\n            return Self.graphBaseURL.appendingPathComponent(\"me/drive/root/children\")\n        }\n        let encoded = encodePathForGraph(absPath)\n        let urlString = \"\\(Self.graphBaseURL.absoluteString)/me/drive/root:\\(encoded):/children\"\n        return URL(string: urlString)!\n    }\n\n    /// Returns the Graph API URL for uploading/downloading file content.\n    func graphURL(forContentAtPath path: String) -> URL {\n        let absPath = absolutePath(for: path)\n        let encoded = encodePathForGraph(absPath)\n        let urlString = \"\\(Self.graphBaseURL.absoluteString)/me/drive/root:\\(encoded):/content\"\n        return URL(string: urlString)!\n    }\n\n    // MARK: - Path Helpers\n\n    func absolutePath(for path: String) -> String {\n        var absPath = path\n        if !absPath.hasPrefix(\"/\") { absPath = \"/\" + absPath }\n        while absPath.contains(\"//\") {\n            absPath = absPath.replacingOccurrences(of: \"//\", with: \"/\")\n        }\n        if absPath != \"/\", absPath.hasSuffix(\"/\") {\n            absPath = String(absPath.dropLast())\n        }\n        return absPath\n    }\n\n    /// Percent-encodes a path for use in Graph API URLs.\n    func encodePathForGraph(_ path: String) -> String {\n        let components = path.split(separator: \"/\", omittingEmptySubsequences: true)\n        let encoded = components.map { component in\n            component.addingPercentEncoding(withAllowedCharacters: .graphPathAllowed) ?? String(component)\n        }\n        return \"/\" + encoded.joined(separator: \"/\")\n    }\n\n    // MARK: - Request Helpers\n\n    private func performRequest(_ request: URLRequest) async throws -> [String: Any] {\n        var req = request\n        req.cachePolicy = .reloadIgnoringLocalCacheData\n        if req.timeoutInterval == 0 { req.timeoutInterval = 60 }\n\n        let (data, response) = try await session.data(for: req)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        if statusCode == 404 {\n            throw CloudFileSystemError.fileNotFound\n        }\n\n        guard (200..<300).contains(statusCode) else {\n            throw mapHTTPError(statusCode: statusCode)\n        }\n\n        return try parseJSON(data)\n    }\n\n    private func parseJSON(_ data: Data) throws -> [String: Any] {\n        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {\n            throw CloudFileSystemError.serverError(statusCode: 0)\n        }\n        return json\n    }\n\n    // MARK: - Error Handling\n\n    func mapHTTPError(statusCode: Int) -> CloudFileSystemError {\n        switch statusCode {\n        case 401: return .authenticationFailed\n        case 404: return .fileNotFound\n        default: return .serverError(statusCode: statusCode)\n        }\n    }\n}\n\n// MARK: - Character Set Extension\n\nprivate extension CharacterSet {\n    /// Characters allowed in OneDrive path components.\n    /// Standard URL path-allowed characters minus colon, hash, and question mark\n    /// (colon is used as the Graph API delimiter).\n    static let graphPathAllowed: CharacterSet = {\n        var cs = CharacterSet.urlPathAllowed\n        cs.remove(charactersIn: \":#?\")\n        return cs\n    }()\n}\n"
  },
  {
    "path": "Sources/LLVSPCloud/PCloudExchange.swift",
    "content": "//\n//  PCloudExchange.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\nimport LLVS\nimport PCloudSDKSwift\n\n/// An Exchange that syncs versions via the pCloud Swift SDK.\n///\n/// Data is stored in a named folder on pCloud, organized as:\n/// - `versions/` — JSON-encoded version metadata, one file per version\n/// - `changes/` — JSON-encoded value changes, one file per version\n///\n/// Uses `PCloudClient` from the official pCloud SDK for all API operations.\n/// Folder IDs are cached via `restorationState` to avoid repeated lookups.\npublic class PCloudExchange: FolderBasedExchange {\n\n    public typealias FileID = UInt64\n    public typealias FolderID = UInt64\n\n    public enum Error: Swift.Error {\n        case missingDownloadLink\n        case invalidResponse\n    }\n\n    public let store: Store\n\n    /// The pCloud client used for API calls. Create via `PCloud.createClient(with:hostProvider:)`.\n    public let client: PCloudClient\n\n    /// Parent folder ID on pCloud where the LLVS folder will be created. 0 = root.\n    public let parentFolderID: UInt64\n\n    /// Name of the folder on pCloud that holds LLVS data.\n    public let folderName: String\n\n    /// URL session used for downloading file data from pCloud CDN links.\n    private let urlSession: URLSession\n\n    @Atomic private var restoration = RestorationInfo()\n\n    public let newVersionsAvailable: AsyncStream<Void>\n    private let newVersionsContinuation: AsyncStream<Void>.Continuation\n\n    public var restorationState: Data? {\n        get { try? JSONEncoder().encode(restoration) }\n        set {\n            if let data = newValue, let info = try? JSONDecoder().decode(RestorationInfo.self, from: data) {\n                restoration = info\n            }\n        }\n    }\n\n    /// - Parameters:\n    ///   - store: The LLVS store to sync.\n    ///   - client: A `PCloudClient` instance, created via `PCloud.createClient(with:hostProvider:)`.\n    ///   - parentFolderID: pCloud folder ID in which to create the LLVS folder. Defaults to 0 (root).\n    ///   - folderName: Name of the folder to create for LLVS data. Defaults to \"LLVS\".\n    ///   - urlSession: URLSession for downloading file content from CDN links. Defaults to `.shared`.\n    public init(store: Store, client: PCloudClient, parentFolderID: UInt64 = 0, folderName: String = \"LLVS\", urlSession: URLSession = .shared) {\n        self.store = store\n        self.client = client\n        self.parentFolderID = parentFolderID\n        self.folderName = folderName\n        self.urlSession = urlSession\n        let (stream, continuation) = AsyncStream<Void>.makeStream()\n        self.newVersionsAvailable = stream\n        self.newVersionsContinuation = continuation\n    }\n\n    // MARK: - Prepare\n\n    public func prepareToRetrieve() async throws {\n        try await ensureFoldersExist()\n    }\n\n    public func prepareToSend() async throws {\n        try await ensureFoldersExist()\n    }\n\n    // MARK: - FolderBasedExchange\n\n    public var versionsFolderID: UInt64? { restoration.versionsFolderID }\n    public var changesFolderID: UInt64? { restoration.changesFolderID }\n\n    public func notifyNewVersionsAvailable() {\n        newVersionsContinuation.yield(())\n    }\n\n    public func listFiles(inFolder folderID: UInt64) async throws -> [String: UInt64] {\n        try await withCheckedThrowingContinuation { continuation in\n            client.listFolder(folderID, recursively: false)\n                .addCompletionBlock { result in\n                    switch result {\n                    case .success(let folder):\n                        var fileMap: [String: UInt64] = [:]\n                        for content in folder.contents {\n                            if case .file(let file) = content {\n                                fileMap[file.name] = file.id\n                            }\n                        }\n                        continuation.resume(returning: fileMap)\n                    case .failure(let error):\n                        continuation.resume(throwing: error)\n                    }\n                }\n                .start()\n        }\n    }\n\n    public func downloadData(forFile fileID: UInt64) async throws -> Data {\n        let link: URL = try await withCheckedThrowingContinuation { continuation in\n            client.getFileLink(forFile: fileID)\n                .addCompletionBlock { result in\n                    switch result {\n                    case .success(let links):\n                        guard let link = links.first else {\n                            continuation.resume(throwing: Error.missingDownloadLink)\n                            return\n                        }\n                        continuation.resume(returning: link.address)\n                    case .failure(let error):\n                        continuation.resume(throwing: error)\n                    }\n                }\n                .start()\n        }\n\n        return try await withCheckedThrowingContinuation { continuation in\n            let task = urlSession.dataTask(with: link) { data, _, error in\n                if let error = error {\n                    continuation.resume(throwing: error)\n                } else if let data = data {\n                    continuation.resume(returning: data)\n                } else {\n                    continuation.resume(throwing: Error.invalidResponse)\n                }\n            }\n            task.resume()\n        }\n    }\n\n    public func uploadData(_ data: Data, named name: String, toFolder folderID: UInt64) async throws {\n        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in\n            client.upload(data, toFolder: folderID, asFileNamed: name)\n                .addCompletionBlock { result in\n                    switch result {\n                    case .success:\n                        continuation.resume(returning: ())\n                    case .failure(let error):\n                        continuation.resume(throwing: error)\n                    }\n                }\n                .start()\n        }\n    }\n\n    // MARK: - pCloud SDK Helpers\n\n    private func ensureFoldersExist() async throws {\n        if restoration.versionsFolderID != nil && restoration.changesFolderID != nil {\n            return\n        }\n\n        let rootID = try await createFolderIfNeeded(named: folderName, inFolder: parentFolderID)\n        restoration.rootFolderID = rootID\n\n        let versionsID = try await createFolderIfNeeded(named: \"versions\", inFolder: rootID)\n        restoration.versionsFolderID = versionsID\n\n        let changesID = try await createFolderIfNeeded(named: \"changes\", inFolder: rootID)\n        restoration.changesFolderID = changesID\n    }\n\n    private func createFolderIfNeeded(named name: String, inFolder parentID: UInt64) async throws -> UInt64 {\n        try await withCheckedThrowingContinuation { continuation in\n            client.createFolderIfDoesNotExist(named: name, inFolder: parentID)\n                .addCompletionBlock { result in\n                    switch result {\n                    case .success(let response):\n                        continuation.resume(returning: response.folder.id)\n                    case .failure(let error):\n                        continuation.resume(throwing: error)\n                    }\n                }\n                .start()\n        }\n    }\n\n    // MARK: - Restoration\n\n    fileprivate struct RestorationInfo: Codable {\n        var rootFolderID: UInt64?\n        var versionsFolderID: UInt64?\n        var changesFolderID: UInt64?\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSSQLite/SQLiteDatabase+Zones.swift",
    "content": "//\n//  SQLiteDatabase+Zones.swift\n//  \n//\n//  Created by Drew McCormack on 16/02/2022.\n//\n\nimport Foundation\nimport LLVS\n\ninternal extension SQLiteDatabase {\n    \n    func setupForZone() throws {\n        try execute(statement:\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS\n                Zone(\n                    key TEXT NOT NULL,\n                    version TEXT NOT NULL,\n                    data BLOB NOT NULL,\n                    PRIMARY KEY (key, version)\n                );\n            \"\"\"\n        )\n        try execute(statement:\n            \"\"\"\n            CREATE INDEX IF NOT EXISTS index_key ON Zone (key);\n            \"\"\"\n        )\n        try execute(statement:\n            \"\"\"\n            CREATE INDEX IF NOT EXISTS index_version ON Zone (version);\n            \"\"\"\n        )\n    }\n    \n    func store(_ data: Data, for reference: ZoneReference) throws {\n        try execute(statement:\n            \"\"\"\n            INSERT OR REPLACE INTO Zone (key, version, data) VALUES (?, ?, ?);\n            \"\"\",\n            withBindingsList: [[reference.key, reference.version.rawValue, data]])\n    }\n    \n    func data(for reference: ZoneReference) throws -> Data? {\n        var result: Data?\n        try forEach(matchingQuery:\n            \"\"\"\n            SELECT data FROM Zone WHERE key=? AND version=?;\n            \"\"\",\n            withBindings: [reference.key, reference.version.rawValue]) { row in\n                result = row.value(inColumnAtIndex: 0)\n            }\n        return result\n    }\n    \n    func data(forReferences references: [(key: String, version: String)]) throws -> [Int: Data] {\n        guard !references.isEmpty else { return [:] }\n\n        // Group references by version, tracking original indices\n        var indicesByVersion: [String: [(index: Int, key: String)]] = [:]\n        for (i, ref) in references.enumerated() {\n            indicesByVersion[ref.version, default: []].append((index: i, key: ref.key))\n        }\n\n        var result: [Int: Data] = [:]\n\n        // One query per distinct version\n        for (version, entries) in indicesByVersion {\n            let placeholders = entries.map { _ in \"?\" }.joined(separator: \", \")\n            let query = \"SELECT key, data FROM Zone WHERE version = ? AND key IN (\\(placeholders));\"\n            var bindings: [Any?] = [version]\n            bindings.append(contentsOf: entries.map { $0.key as Any? })\n\n            // Build lookup from key to indices (multiple refs can share a key within the same version)\n            var indexByKey: [String: [Int]] = [:]\n            for entry in entries {\n                indexByKey[entry.key, default: []].append(entry.index)\n            }\n\n            try forEach(matchingQuery: query, withBindings: bindings) { row in\n                let key: String = row.value(inColumnAtIndex: 0)!\n                let data: Data = row.value(inColumnAtIndex: 1)!\n                if let indices = indexByKey[key] {\n                    for index in indices {\n                        result[index] = data\n                    }\n                }\n            }\n        }\n\n        return result\n    }\n\n    func versionIds(forKey key: String) throws -> [String] {\n        var versionStrings: [String] = []\n        try forEach(matchingQuery:\n            \"\"\"\n            SELECT DISTINCT version FROM Zone WHERE key=?;\n            \"\"\",\n            withBindings: [key]) { row in\n                let versionId: String = row.value(inColumnAtIndex: 0)!\n                versionStrings.append(versionId)\n            }\n        return versionStrings\n    }\n    \n}\n"
  },
  {
    "path": "Sources/LLVSSQLite/SQLiteDatabase.swift",
    "content": "//\n//  Database.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 19/01/2017.\n//\n\nimport Foundation\nimport SQLite3\n\n// Used for SQLite cleanup\ninternal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self)\ninternal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)\n\n/// Thin wrapper around SQLite3. Allows creating, updating, and querying a SQLite3 database\n/// with as little fuss as possible.\n/// This class is not thread-safe. It is up to the\n/// user of the class to ensure it is only accessed from one thread at a time (e.g. using\n/// a serial queue). The decision not to use an internal queue is deliberate:\n/// this gives the caller complete freedom in how the database is accessed, rather than\n/// imposing a particular solution (e.g. GCD, OperationQueue, Actor). It keeps database \n/// access independent of the preferred concurrency model used in the app.\npublic final class SQLiteDatabase {\n\n    /// SQLite Errors\n    public enum Error: Swift.Error {\n        case openFailed(code: Int32)\n        case closeFailed(code: Int32)\n        case statementFailed(statement: String, code: Int32)\n        case bindingFailed(bindingIndex: Int, value: Any?, code: Int32)\n        case queryFailed(query: String, code: Int32)\n    }\n    \n    /// Location of the database file\n    public let fileURL: URL\n    \n    /// Pointer to the SQLite database object\n    private var database: OpaquePointer!\n    \n    /// The last error message from the database\n    public var errorMessage: String? {\n        return String(cString: sqlite3_errmsg(database))\n    }\n    \n    \n    // MARK: Creating and Deleting\n    \n    /// Open or create a database at the file URL passed in\n    public init(fileURL: URL) throws {\n        self.fileURL = fileURL\n        \n        var newDatabase: OpaquePointer?\n        let code = sqlite3_open(fileURL.path, &newDatabase)\n        guard code == SQLITE_OK else {\n            throw Error.openFailed(code: code)\n        }\n        database = newDatabase!\n    }\n    \n    deinit {\n        do {\n            try close()\n        } catch {\n            print(\"Could not close database: \\(error)\")\n        }\n    }\n    \n    \n    // MARK: Executing SQL\n    \n    /// Explicitly close the database. If you don't call this, it will be called in deinit.\n    public func close() throws {\n        guard let database = database else { return }\n        let code = sqlite3_close(database)\n        if code != SQLITE_OK {\n            throw Error.closeFailed(code: code)\n        }\n        self.database = nil\n    }\n    \n    /// Execute a SQLite statement with bindings provided by an array of arrays. \n    /// Each entry in the outer array is an array holding the bindings for one statement.\n    /// The statement will be executed multiple times with different values.\n    /// Passing nothing for the bindings can be used to execute a statement that has no bound values.\n    /// Passing an empty array for the bindings is allowed, but is effectively a no-op.\n    public func execute(statement: String, withBindingsList bindings: [[Any?]] = [[]]) throws {\n        var sqlStatement: OpaquePointer?\n        let prepareCode = sqlite3_prepare_v2(database, statement, -1, &sqlStatement, nil)\n        guard prepareCode == SQLITE_OK else {\n            throw Error.statementFailed(statement: statement, code: prepareCode)\n        }\n        \n        defer {\n            finalize(sqlStatement!)\n        }\n    \n        for boundValues in bindings {\n            try bind(values: boundValues, toSQLStatement: sqlStatement!)\n            \n            let stepCode = sqlite3_step(sqlStatement)\n            guard  stepCode == SQLITE_DONE else {\n                throw Error.statementFailed(statement: statement, code: stepCode)\n            }\n            \n            let resetCode = sqlite3_reset(sqlStatement)\n            guard resetCode == SQLITE_OK else {\n                throw Error.statementFailed(statement: statement, code: resetCode)\n            }\n        }\n    }\n    \n    /// Fetch with a query, and iterate the results in the row handler.\n    /// The row should not be allowed to escape the row handler block, and must remain on the same thread.\n    /// In general, you should extract the data from the `Row` directly, and once that is done, you can treat\n    /// the resulting data as you please.\n    public func forEach(matchingQuery query: String, withBindings bindings: [Any?] = [], rowHandler: (Row) throws ->Void ) throws {\n        var sqlStatement: OpaquePointer?\n        let prepareCode = sqlite3_prepare_v2(database, query, -1, &sqlStatement, nil)\n        guard prepareCode == SQLITE_OK else {\n            throw Error.queryFailed(query: query, code: prepareCode)\n        }\n        \n        defer {\n            finalize(sqlStatement!)\n        }\n        \n        try bind(values: bindings, toSQLStatement: sqlStatement!)\n        \n        while sqlite3_step(sqlStatement) == SQLITE_ROW {\n            let row = Row(statement: sqlStatement!)\n            try rowHandler(row)\n        }\n    }\n    \n    /// Finalizes the statement. This func does not throw, because it is often called\n    /// in a defer block, where errors go unhandled.\n    private func finalize(_ sqlStatement: OpaquePointer) {\n        let finalizeCode = sqlite3_finalize(sqlStatement)\n        if finalizeCode != SQLITE_OK {\n            print(\"Failed to finalize statement\")\n        }\n    }\n    \n    /// Binds the passed values to the corresponding placeholders in the compiled SQL statement.\n    private func bind(values: [Any?], toSQLStatement sqlStatement: OpaquePointer) throws {\n        for (i, value) in values.enumerated() {\n            let bindIndex = Int32(i + 1)\n            var code = SQLITE_OK\n            switch value {\n            case nil:\n                code = sqlite3_bind_null(sqlStatement, bindIndex)\n            case let text? as String?:\n                let utf8String = text.cString(using: .utf8)\n                code = sqlite3_bind_text(sqlStatement, bindIndex, utf8String, -1, SQLITE_TRANSIENT)\n            case let data? as Data?:\n                let bytes = [UInt8](data)\n                code = sqlite3_bind_blob(sqlStatement, bindIndex, bytes, Int32(data.count), SQLITE_TRANSIENT)\n            case let intValue? as Int32?:\n                code = sqlite3_bind_int(sqlStatement, bindIndex, intValue)\n            case let intValue? as Int64?:\n                code = sqlite3_bind_int64(sqlStatement, bindIndex, intValue)\n            case let doubleValue? as Double?:\n                code = sqlite3_bind_double(sqlStatement, bindIndex, doubleValue)\n            default:\n                preconditionFailure(\"Invalid type\")\n            }\n            \n            guard code == SQLITE_OK else {\n                throw Error.bindingFailed(bindingIndex: i+1, value: value, code: code)\n            }\n        }\n    }\n    \n    \n    // MARK: Row\n\n    /// Row struct used in fetches\n    public struct Row {\n        \n        private let statement: OpaquePointer\n        \n        fileprivate init(statement: OpaquePointer) {\n            self.statement = statement\n        }\n        \n        /// Retrieve a value from the database, in the column passed.\n        /// Only SQLite types are supported (Int32, Int64, Double, String, Data),\n        /// as well as optional variants. Column indexes are zero based.\n        public func value<T>(inColumnAtIndex column: Int) -> T? {\n            if sqlite3_column_type(statement, 0) == SQLITE_NULL {\n                return nil\n            }\n            \n            switch T.self {\n            case is String.Type:\n                let cString = sqlite3_column_text(statement, Int32(column))\n                return String(cString: cString!) as? T\n            case is Data.Type:\n                let length = sqlite3_column_bytes(statement, Int32(column))\n                let bytes = sqlite3_column_blob(statement, Int32(column))\n                let data = Data(bytes: bytes!, count: Int(length))\n                return data as? T\n            case is Int64.Type:\n                return sqlite3_column_int64(statement, Int32(column)) as? T\n            case is Int32.Type:\n                return Int32(sqlite3_column_int(statement, Int32(column))) as? T\n            case is Double.Type:\n                return sqlite3_column_double(statement, Int32(column)) as? T\n            default:\n                preconditionFailure(\"Unsupported type\")\n            }\n        }\n\n    }\n\n}\n\n"
  },
  {
    "path": "Sources/LLVSSQLite/SQLiteZone.swift",
    "content": "//\n//  SQLiteZone.swift\n//  LLVS\n//\n//  Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\nimport LLVS\nimport SQLite3\n\npublic class SQLiteStorage: Storage, SnapshotCapable {\n\n    private let fileExtension = \"sqlite\"\n\n    public init() {}\n\n    public func makeMapZone(for type: MapType, in store: Store) throws -> Zone {\n        switch type {\n        case .valuesByVersion:\n            return try SQLiteZone(rootDirectory: store.valuesMapDirectoryURL, fileExtension: fileExtension)\n        case .userDefined:\n            fatalError(\"User defined maps not yet supported\")\n        }\n    }\n\n    public func makeValuesZone(in store: Store) throws -> Zone {\n        return try SQLiteZone(rootDirectory: store.valuesDirectoryURL, fileExtension: fileExtension)\n    }\n\n}\n\ninternal final class SQLiteZone: Zone {\n    \n    let rootDirectory: URL\n    let fileExtension: String\n\n    private let fileURL: URL\n    private let database: SQLiteDatabase\n    \n    private let uncachableDataSizeLimit = 10000 // 10KB\n    private let cache: Cache<Data> = .init()\n    \n    fileprivate let fileManager = FileManager()\n    \n    init(rootDirectory: URL, fileExtension: String) throws {\n        let resolvedURL = rootDirectory.resolvingSymlinksInPath()\n        self.rootDirectory = resolvedURL\n        try? fileManager.createDirectory(at: rootDirectory, withIntermediateDirectories: true, attributes: nil)\n        self.fileExtension = fileExtension\n        self.fileURL = resolvedURL.appendingPathComponent(\"zone\").appendingPathExtension(fileExtension)\n        database = try SQLiteDatabase(fileURL: self.fileURL)\n        try database.setupForZone()\n    }\n    \n    internal func dismantle() throws {\n        try database.close()\n    }\n    \n    private func cacheIfNeeded(_ data: Data, for reference: ZoneReference) {\n        if data.count < uncachableDataSizeLimit {\n            cache.setValue(data, for: reference)\n        }\n    }\n    \n    internal func store(_ data: Data, for reference: ZoneReference) throws {\n        let compressed = DataCompression.compress(data)\n        try database.store(compressed, for: reference)\n        cacheIfNeeded(data, for: reference)\n    }\n\n    internal func data(for reference: ZoneReference) throws -> Data? {\n        if let data = cache.value(for: reference) { return data }\n        guard let raw = try database.data(for: reference) else { return nil }\n        let data = DataCompression.decompressIfNeeded(raw)\n        cacheIfNeeded(data, for: reference)\n        return data\n    }\n    \n    internal func data(for references: [ZoneReference]) throws -> [Data?] {\n        guard !references.isEmpty else { return [] }\n\n        // Check cache first, collect uncached references with their indices\n        var results = [Data?](repeating: nil, count: references.count)\n        var uncached: [(index: Int, key: String, version: String)] = []\n        for (i, ref) in references.enumerated() {\n            if let data = cache.value(for: ref) {\n                results[i] = data\n            } else {\n                uncached.append((index: i, key: ref.key, version: ref.version.rawValue))\n            }\n        }\n\n        if !uncached.isEmpty {\n            let tuples = uncached.map { (key: $0.key, version: $0.version) }\n            let fetched = try database.data(forReferences: tuples)\n            for (localIndex, entry) in uncached.enumerated() {\n                if let raw = fetched[localIndex] {\n                    let data = DataCompression.decompressIfNeeded(raw)\n                    results[entry.index] = data\n                    cacheIfNeeded(data, for: references[entry.index])\n                }\n            }\n        }\n\n        return results\n    }\n\n    internal func versionIds(for key: String) throws -> [Version.ID] {\n        try database.versionIds(forKey: key).map { Version.ID($0) }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSWebDAV/WebDAVFileSystem.swift",
    "content": "//\n//  WebDAVFileSystem.swift\n//  LLVSWebDAV\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// A `CloudFileSystem` backed by a WebDAV server using `URLSession`.\n///\n/// No third-party dependencies — uses standard HTTP methods\n/// (PROPFIND, MKCOL, PUT, GET, DELETE).\n///\n/// Authentication is handled via `URLCredential`, supporting both Basic and\n/// Digest auth (Digest is handled automatically by URLSession's challenge mechanism).\npublic final class WebDAVFileSystem: CloudFileSystem, @unchecked Sendable {\n\n    // MARK: - Properties\n\n    /// The base URL of the WebDAV server.\n    public let baseURL: URL\n\n    /// The credential used for authentication.\n    public var credential: URLCredential?\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.default\n        config.timeoutIntervalForRequest = 60\n        config.timeoutIntervalForResource = 3600\n        let delegate = SessionDelegate(fileSystem: self)\n        return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)\n    }()\n\n    // MARK: - Initialization\n\n    /// Creates a WebDAV file system with the given base URL.\n    /// - Parameters:\n    ///   - baseURL: The root URL of the WebDAV server.\n    ///   - username: Optional username for authentication.\n    ///   - password: Optional password for authentication.\n    public init(baseURL: URL, username: String? = nil, password: String? = nil) {\n        self.baseURL = baseURL\n        if let username, let password {\n            self.credential = URLCredential(user: username, password: password, persistence: .forSession)\n        }\n    }\n\n    // MARK: - CloudFileSystem\n\n    public func fileExists(at path: String) async throws -> Bool {\n        let request = makePropfindRequest(forPath: path, depth: 0)\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n        if statusCode == 404 { return false }\n        try checkHTTPResponse(statusCode: statusCode)\n        return true\n    }\n\n    public func contentsOfDirectory(at path: String) async throws -> [String] {\n        var dirPath = path\n        if !dirPath.hasSuffix(\"/\") { dirPath += \"/\" }\n\n        let request = makePropfindRequest(forPath: dirPath, depth: 1)\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        if statusCode == 404 {\n            throw CloudFileSystemError.fileNotFound\n        }\n        try checkHTTPResponse(statusCode: statusCode)\n\n        let parser = WebDAVResponseParser(data: data)\n        try parser.parse()\n\n        // The first item is the directory itself — skip it\n        var items = parser.parsedItems\n        if items.count > 1 {\n            items = Array(items.dropFirst())\n        } else {\n            items = []\n        }\n\n        // Return only file names (not directories)\n        return items.filter { !$0.isDirectory }.map { $0.name }\n    }\n\n    public func upload(data: Data, to path: String) async throws {\n        // Create intermediate directories\n        try await createIntermediateDirectories(for: path)\n\n        var request = makeRequest(forPath: path)\n        request.httpMethod = \"PUT\"\n        request.setValue(\"application/octet-stream\", forHTTPHeaderField: \"Content-Type\")\n        request.setValue(\"\\(data.count)\", forHTTPHeaderField: \"Content-Length\")\n        request.timeoutInterval = 3600\n\n        let (_, response) = try await session.upload(for: request, from: data)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n        try checkHTTPResponse(statusCode: statusCode)\n    }\n\n    public func download(from path: String) async throws -> Data {\n        var request = makeRequest(forPath: path)\n        request.httpMethod = \"GET\"\n\n        let (data, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        if statusCode == 404 {\n            throw CloudFileSystemError.fileNotFound\n        }\n        try checkHTTPResponse(statusCode: statusCode)\n        return data\n    }\n\n    public func remove(at path: String) async throws {\n        var request = makeRequest(forPath: path)\n        request.httpMethod = \"DELETE\"\n\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        // 404 means already gone — not an error\n        if statusCode == 404 { return }\n        try checkHTTPResponse(statusCode: statusCode)\n    }\n\n    public func removeDirectory(at path: String) async throws {\n        // WebDAV DELETE on a collection removes it recursively\n        var dirPath = path\n        if !dirPath.hasSuffix(\"/\") { dirPath += \"/\" }\n\n        var request = makeRequest(forPath: dirPath)\n        request.httpMethod = \"DELETE\"\n\n        let (_, response) = try await session.data(for: request)\n        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n        // 404 means already gone — not an error\n        if statusCode == 404 { return }\n        try checkHTTPResponse(statusCode: statusCode)\n    }\n\n    // MARK: - Directory Creation\n\n    private func createIntermediateDirectories(for path: String) async throws {\n        let components = path.split(separator: \"/\").dropLast() // drop the filename\n        var currentPath = \"\"\n        for component in components {\n            currentPath += \"/\\(component)\"\n            // Check if directory exists\n            let request = makePropfindRequest(forPath: currentPath + \"/\", depth: 0)\n            let (_, response) = try await session.data(for: request)\n            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0\n\n            if statusCode == 404 {\n                // Create the directory\n                var mkcolRequest = makeRequest(forPath: currentPath)\n                mkcolRequest.httpMethod = \"MKCOL\"\n                mkcolRequest.setValue(\"application/xml\", forHTTPHeaderField: \"Content-Type\")\n                let (_, mkcolResponse) = try await session.data(for: mkcolRequest)\n                let mkcolStatus = (mkcolResponse as? HTTPURLResponse)?.statusCode ?? 0\n                // 405 Method Not Allowed means the directory already exists\n                if mkcolStatus != 405 {\n                    try checkHTTPResponse(statusCode: mkcolStatus)\n                }\n            }\n        }\n    }\n\n    // MARK: - Request Building\n\n    private func makeRequest(forPath path: String) -> URLRequest {\n        let url = baseURL.appendingPathComponent(path)\n        return URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60)\n    }\n\n    private func makePropfindRequest(forPath path: String, depth: Int) -> URLRequest {\n        var request = makeRequest(forPath: path)\n        request.httpMethod = \"PROPFIND\"\n        request.setValue(\"\\(depth)\", forHTTPHeaderField: \"Depth\")\n        request.setValue(\"application/xml\", forHTTPHeaderField: \"Content-Type\")\n\n        let xml = \"\"\"\n        <?xml version=\"1.0\" encoding=\"utf-8\" ?>\\\n        <D:propfind xmlns:D=\"DAV:\">\\\n        <D:prop>\\\n        <D:href/>\\\n        <D:resourcetype/>\\\n        <D:getcontentlength/>\\\n        </D:prop>\\\n        </D:propfind>\n        \"\"\"\n        request.httpBody = xml.data(using: .utf8)\n        return request\n    }\n\n    // MARK: - Response Handling\n\n    private func checkHTTPResponse(statusCode: Int) throws {\n        if statusCode == 401 {\n            throw CloudFileSystemError.authenticationFailed\n        }\n        guard (200..<300).contains(statusCode) || statusCode == 207 else {\n            throw CloudFileSystemError.serverError(statusCode: statusCode)\n        }\n    }\n}\n\n// MARK: - URLSession Delegate for Authentication\n\nprivate final class SessionDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate, @unchecked Sendable {\n\n    weak var fileSystem: WebDAVFileSystem?\n\n    init(fileSystem: WebDAVFileSystem) {\n        self.fileSystem = fileSystem\n    }\n\n    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {\n        let protectionSpace = challenge.protectionSpace\n\n        if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust ||\n           protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {\n            completionHandler(.performDefaultHandling, nil)\n            return\n        }\n\n        if let credential = fileSystem?.credential, challenge.previousFailureCount == 0 {\n            completionHandler(.useCredential, credential)\n        } else {\n            completionHandler(.performDefaultHandling, nil)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LLVSWebDAV/WebDAVResponseParser.swift",
    "content": "//\n//  WebDAVResponseParser.swift\n//  LLVSWebDAV\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/// Parses PROPFIND XML responses from a WebDAV server.\n/// Handles namespace variations (`D:`, `lp1:`, etc.) for broad server compatibility.\nfinal class WebDAVResponseParser: NSObject, XMLParserDelegate, @unchecked Sendable {\n\n    struct Item {\n        let name: String\n        let isDirectory: Bool\n    }\n\n    private let xmlParser: XMLParser\n    private var items: [Item] = []\n    private var characters = \"\"\n    private var currentItemDictionary: [String: Any]?\n\n    var parsedItems: [Item] { items }\n\n    init(data: Data) {\n        xmlParser = XMLParser(data: data)\n        super.init()\n        xmlParser.delegate = self\n    }\n\n    func parse() throws {\n        let success = xmlParser.parse()\n        if !success {\n            throw xmlParser.parserError ?? CloudFileSystemError.serverError(statusCode: 0)\n        }\n    }\n\n    // MARK: - Element Matching\n\n    /// Matches element names with or without common WebDAV namespace prefixes.\n    private func element(_ element: String, matches other: String) -> Bool {\n        if element.caseInsensitiveCompare(other) == .orderedSame { return true }\n        if (\"D:\" + element).caseInsensitiveCompare(other) == .orderedSame { return true }\n        if (\"lp1:\" + element).caseInsensitiveCompare(other) == .orderedSame { return true }\n        return false\n    }\n\n    // MARK: - XMLParserDelegate\n\n    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {\n        characters = \"\"\n        if element(elementName, matches: \"D:response\") {\n            currentItemDictionary = [:]\n        }\n    }\n\n    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {\n        if element(elementName, matches: \"D:href\") {\n            currentItemDictionary?[\"path\"] = characters.removingPercentEncoding ?? characters\n        } else if element(elementName, matches: \"D:collection\") {\n            currentItemDictionary?[\"isDirectory\"] = true\n        } else if element(elementName, matches: \"D:response\"),\n                  let dict = currentItemDictionary,\n                  let path = dict[\"path\"] as? String {\n            let isDir = dict[\"isDirectory\"] as? Bool ?? false\n            let name = (path as NSString).lastPathComponent\n            items.append(Item(name: name, isDirectory: isDir))\n            currentItemDictionary = nil\n        }\n    }\n\n    func parser(_ parser: XMLParser, foundCharacters string: String) {\n        characters += string\n    }\n}\n"
  },
  {
    "path": "Sources/SQLite3/module.modulemap",
    "content": "module SQLite {\n    header \"shim.h\"\n    link \"sqlite3\"\n    export *\n}\n"
  },
  {
    "path": "Sources/SQLite3/shim.h",
    "content": "#include <sqlite3.h>\n"
  },
  {
    "path": "Tests/LLVSModelTests/MergeableArbiterTests.swift",
    "content": "//\n//  MergeableArbiterTests.swift\n//  LLVSModelTests\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n@testable import LLVSModel\n\n// MARK: - Test Model Types\n\n@MergeableModel\nstruct Contact: StorableModel, Equatable {\n    static let modelTypeIdentifier = \"Contact\"\n    var name: String = \"\"\n    var notes: String = \"\"\n    var age: Int = 0\n}\n\n@MergeableModel\nstruct Tag: StorableModel, Equatable {\n    static let modelTypeIdentifier = \"Tag\"\n    var label: String = \"\"\n}\n\n@MergeableModel\nstruct InnerModel: Codable, Equatable {\n    var x: Int = 0\n    var y: Int = 0\n}\n\n@MergeableModel\nstruct OuterModel: StorableModel, Equatable {\n    static let modelTypeIdentifier = \"OuterModel\"\n    var inner: InnerModel = InnerModel()\n    var label: String = \"\"\n}\n\n@MergeableModel\nstruct OuterOptionalModel: StorableModel, Equatable {\n    static let modelTypeIdentifier = \"OuterOptionalModel\"\n    var inner: InnerModel? = nil\n    var label: String = \"\"\n}\n\n// MARK: - Tests\n\n@Suite class MergeableArbiterTests {\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    // MARK: - Helpers\n\n    private func makeVersion(basedOn predecessor: Version.ID?, changes: [Value.Change]) -> Version {\n        try! store.makeVersion(basedOnPredecessor: predecessor, storing: changes)\n    }\n\n    private func encode<T: Codable>(_ value: T) -> Data {\n        try! JSONEncoder().encode(value)\n    }\n\n    // MARK: - Property-wise 3-way Merge\n\n    @Test func threeWayMergeResolvesPropertyWise() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(Contact.self)\n\n        let valueId = modelValueID(typeIdentifier: \"Contact\", instanceIdentifier: \"abc\")\n\n        // Ancestor: name=\"Alice\", notes=\"Hello\", age=30\n        let ancestor = Contact(name: \"Alice\", notes: \"Hello\", age: 30)\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: change name to \"Bob\"\n        var branch1Contact = ancestor\n        branch1Contact.name = \"Bob\"\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(branch1Contact)))])\n\n        // Branch 2: change age to 31\n        var branch2Contact = ancestor\n        branch2Contact.age = 31\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(branch2Contact)))])\n\n        // Merge\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let contact = try JSONDecoder().decode(Contact.self, from: result.data)\n\n        #expect(contact.name == \"Bob\")\n        #expect(contact.age == 31)\n        #expect(contact.notes == \"Hello\")\n    }\n\n    // MARK: - Twice Inserted (salvaging)\n\n    @Test func twiceInsertedUsesSalvaging() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(Contact.self)\n\n        let valueId = modelValueID(typeIdentifier: \"Contact\", instanceIdentifier: \"new\")\n\n        // Two branches independently insert the same key (no common ancestor with that value)\n        let v0 = makeVersion(basedOn: nil, changes: [])\n\n        let c1 = Contact(name: \"Alice\", notes: \"\", age: 25)\n        let v1 = makeVersion(basedOn: v0.id, changes: [.insert(Value(id: valueId, data: encode(c1)))])\n\n        let c2 = Contact(name: \"Bob\", notes: \"\", age: 30)\n        let v2 = makeVersion(basedOn: v0.id, changes: [.insert(Value(id: valueId, data: encode(c2)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let contact = try JSONDecoder().decode(Contact.self, from: result.data)\n\n        // Default salvaging returns self (dominant/first), so first branch wins\n        #expect(contact.name == \"Alice\")\n    }\n\n    // MARK: - Removed and Updated\n\n    @Test func removedAndUpdatedFavorsUpdate() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(Contact.self)\n\n        let valueId = modelValueID(typeIdentifier: \"Contact\", instanceIdentifier: \"ru\")\n\n        let ancestor = Contact(name: \"Alice\", notes: \"\", age: 30)\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: remove\n        let v1 = makeVersion(basedOn: v0.id, changes: [.remove(valueId)])\n\n        // Branch 2: update\n        var updated = ancestor\n        updated.name = \"Bob\"\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(updated)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)\n        #expect(result != nil, \"Value should be preserved (update wins over remove)\")\n\n        let contact = try JSONDecoder().decode(Contact.self, from: result!.data)\n        #expect(contact.name == \"Bob\")\n    }\n\n    // MARK: - Fallback Arbiter\n\n    @Test func unregisteredTypeFallsBackToDefaultArbiter() throws {\n        let arbiter = MergeableArbiter()\n        // Intentionally do NOT register any types\n\n        let valueId = Value.ID(\"untyped/thing\")\n\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: \"old\".data(using: .utf8)!))])\n\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: \"branch1\".data(using: .utf8)!))])\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: \"branch2\".data(using: .utf8)!))])\n\n        // Should not throw — fallback arbiter resolves it\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)\n        #expect(result != nil)\n    }\n\n    // MARK: - StoreCoordinator Typed Save/Fetch\n\n    @Test func storeCoordinatorSaveAndFetchRoundTrip() throws {\n        let cacheURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL)\n        defer { try? FileManager.default.removeItem(at: cacheURL) }\n\n        let contact = Contact(name: \"Alice\", notes: \"Met at WWDC\", age: 30)\n        try coordinator.save(contact, instanceIdentifier: \"abc\")\n\n        let fetched: Contact? = try coordinator.fetchModel(Contact.self, instanceIdentifier: \"abc\")\n        #expect(fetched?.name == \"Alice\")\n        #expect(fetched?.notes == \"Met at WWDC\")\n        #expect(fetched?.age == 30)\n    }\n\n    // MARK: - fetchAllModels\n\n    @Test func fetchAllModelsReturnsCorrectTypeSubset() throws {\n        let cacheURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL)\n        defer { try? FileManager.default.removeItem(at: cacheURL) }\n\n        try coordinator.save(Contact(name: \"Alice\", notes: \"\", age: 30), instanceIdentifier: \"a\")\n        try coordinator.save(Contact(name: \"Bob\", notes: \"\", age: 25), instanceIdentifier: \"b\")\n        try coordinator.save(Tag(label: \"VIP\"), instanceIdentifier: \"t1\")\n\n        let contacts: [Contact] = try coordinator.fetchAllModels(Contact.self)\n        #expect(contacts.count == 2)\n\n        let tags: [Tag] = try coordinator.fetchAllModels(Tag.self)\n        #expect(tags.count == 1)\n        #expect(tags.first?.label == \"VIP\")\n    }\n\n    // MARK: - Value.ID Helpers\n\n    @Test func modelValueIDHelpers() {\n        let valueId = modelValueID(typeIdentifier: \"Contact\", instanceIdentifier: \"abc-123\")\n        #expect(valueId.rawValue == \"Contact/abc-123\")\n        #expect(modelTypeIdentifier(from: valueId) == \"Contact\")\n        #expect(instanceIdentifier(from: valueId) == \"abc-123\")\n    }\n\n    @Test func helperReturnsNilForNoSlash() {\n        let valueId = Value.ID(\"noslash\")\n        #expect(modelTypeIdentifier(from: valueId) == nil)\n        #expect(instanceIdentifier(from: valueId) == nil)\n    }\n\n    // MARK: - Nested Mergeable\n\n    @Test func nestedMergeablePreservesBothBranchChanges() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(OuterModel.self)\n\n        let valueId = modelValueID(typeIdentifier: \"OuterModel\", instanceIdentifier: \"o1\")\n\n        let ancestor = OuterModel(inner: InnerModel(x: 1, y: 2), label: \"start\")\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: change inner.x\n        var b1 = ancestor\n        b1.inner.x = 10\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b1)))])\n\n        // Branch 2: change inner.y\n        var b2 = ancestor\n        b2.inner.y = 20\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b2)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let outer = try JSONDecoder().decode(OuterModel.self, from: result.data)\n\n        #expect(outer.inner.x == 10)\n        #expect(outer.inner.y == 20)\n        #expect(outer.label == \"start\")\n    }\n\n    // MARK: - Optional Mergeable\n\n    @Test func optionalMergeableBothSomePreservesSubproperties() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(OuterOptionalModel.self)\n\n        let valueId = modelValueID(typeIdentifier: \"OuterOptionalModel\", instanceIdentifier: \"om1\")\n\n        let ancestor = OuterOptionalModel(inner: InnerModel(x: 1, y: 2), label: \"start\")\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: change inner.x\n        var b1 = ancestor\n        b1.inner!.x = 10\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b1)))])\n\n        // Branch 2: change inner.y\n        var b2 = ancestor\n        b2.inner!.y = 20\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b2)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let outer = try JSONDecoder().decode(OuterOptionalModel.self, from: result.data)\n\n        #expect(outer.inner?.x == 10)\n        #expect(outer.inner?.y == 20)\n    }\n\n    @Test func optionalMergeableNilToSome() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(OuterOptionalModel.self)\n\n        let valueId = modelValueID(typeIdentifier: \"OuterOptionalModel\", instanceIdentifier: \"om2\")\n\n        let ancestor = OuterOptionalModel(inner: nil, label: \"start\")\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: set inner\n        var b1 = ancestor\n        b1.inner = InnerModel(x: 5, y: 6)\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b1)))])\n\n        // Branch 2: no change to inner\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(ancestor)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let outer = try JSONDecoder().decode(OuterOptionalModel.self, from: result.data)\n\n        // Dominant (v1) inserted inner, ancestor was nil → dominant wins\n        #expect(outer.inner?.x == 5)\n        #expect(outer.inner?.y == 6)\n    }\n\n    @Test func optionalMergeableSomeToNil() throws {\n        let arbiter = MergeableArbiter()\n        arbiter.register(OuterOptionalModel.self)\n\n        let valueId = modelValueID(typeIdentifier: \"OuterOptionalModel\", instanceIdentifier: \"om3\")\n\n        let ancestor = OuterOptionalModel(inner: InnerModel(x: 1, y: 2), label: \"start\")\n        let v0 = makeVersion(basedOn: nil, changes: [.insert(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 1: no change\n        let v1 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(ancestor)))])\n\n        // Branch 2: remove inner\n        var b2 = ancestor\n        b2.inner = nil\n        let v2 = makeVersion(basedOn: v0.id, changes: [.update(Value(id: valueId, data: encode(b2)))])\n\n        let merged = try store.mergeRelated(version: v1.id, with: v2.id, resolvingWith: arbiter)\n        let result = try store.value(id: valueId, at: merged.id)!\n        let outer = try JSONDecoder().decode(OuterOptionalModel.self, from: result.data)\n\n        // Dominant unchanged from ancestor, subordinate removed → accept removal\n        #expect(outer.inner == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/ArrayDiffTests.swift",
    "content": "//\n//  ArrayDiffTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 02/04/2019.\n//\n\nimport Testing\n@testable import LLVS\n\n@Suite struct ArrayDiffTests {\n\n    var diff: LongestCommonSubsequence<Int>!\n\n    @Test mutating func simpleSequence() {\n        diff = LongestCommonSubsequence(originalValues: [1,3], finalValues: [1,2])\n        #expect(diff.length == 1)\n        #expect(diff.originalIndexesOfCommonElements == [0])\n        #expect(diff.finalIndexesOfCommonElements == [0])\n        #expect(diff.incrementalChanges == [.delete(originalIndex: 1, value: 3), .insert(finalIndex: 1, value: 2)])\n    }\n\n    @Test mutating func differingFirstElement() {\n        diff = LongestCommonSubsequence(originalValues: [1,3], finalValues: [2,3])\n        #expect(diff.length == 1)\n        #expect(diff.originalIndexesOfCommonElements == [1])\n        #expect(diff.finalIndexesOfCommonElements == [1])\n        #expect(diff.incrementalChanges == [.delete(originalIndex: 0, value: 1), .insert(finalIndex: 0, value: 2)])\n    }\n\n    @Test mutating func removingFromSequence() {\n        diff = LongestCommonSubsequence(originalValues: [1,2,3,4,5,6,7], finalValues: [1,2,4,5,7])\n        #expect(diff.length == 5)\n        #expect(diff.originalIndexesOfCommonElements == [0,1,3,4,6])\n        #expect(diff.finalIndexesOfCommonElements == [0,1,2,3,4])\n        #expect(diff.incrementalChanges == [.delete(originalIndex: 5, value: 6), .delete(originalIndex: 2, value: 3)])\n    }\n\n    @Test mutating func addingAndRemovingSequence() {\n        diff = LongestCommonSubsequence(originalValues: [1,2,3,4,5,6,7], finalValues: [2,33,4,36,55,6,7])\n        #expect(diff.length == 4)\n        #expect(diff.originalIndexesOfCommonElements == [1,3,5,6])\n        #expect(diff.finalIndexesOfCommonElements == [0,2,5,6])\n        #expect(diff.incrementalChanges == [.delete(originalIndex: 4, value: 5), .delete(originalIndex: 2, value: 3), .delete(originalIndex: 0, value: 1), .insert(finalIndex: 1, value: 33), .insert(finalIndex: 3, value: 36), .insert(finalIndex: 4, value: 55)])\n    }\n\n    @Test func arrayFuncs() {\n        let original = [1,2,3,4,5,6,7]\n        let new = [2,33,4,36,55,6,7]\n        let diff = original.diff(leadingTo: new)\n        #expect(new == original.applying(diff))\n    }\n\n    @Test func empty() {\n        let original: [Int] = []\n        let new: [Int] = []\n        let diff = original.diff(leadingTo: new)\n        #expect([] == original.applying(diff))\n    }\n\n    @Test func originalEmpty() {\n        let original: [Int] = []\n        let new: [Int] = [1,2,3]\n        let diff = original.diff(leadingTo: new)\n        #expect(new == original.applying(diff))\n    }\n\n    @Test func finalEmpty() {\n        let original: [Int] = [1,2,3]\n        let new: [Int] = []\n        let diff = original.diff(leadingTo: new)\n        #expect(new == original.applying(diff))\n    }\n\n    @Test func merge() {\n        let original: [Int] = [1,2]\n        let new1: [Int] = [1,4]\n        let new2: [Int] = [1,2,3]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,4,3] == original.applying(mergeDiff))\n    }\n\n    @Test func mergeLongSequence() {\n        let original: [Int] = [1,2,3,4,5,6,7,8,9,10]\n        let new1: [Int] = [1,2,3,4,5,5,7,8,9,10]\n        let new2: [Int] = [1,2,3,4,5,6,7,8,10,10]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,2,3,4,5,5,7,8,10,10] == original.applying(mergeDiff))\n    }\n\n    @Test func twoDeleteMerge() {\n        let original: [Int] = [1,2,3]\n        let new1: [Int] = [2,3]\n        let new2: [Int] = [2,3,4]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([2,3,4] == original.applying(mergeDiff))\n    }\n\n    @Test func fourDeleteMerge() {\n        let original: [Int] = [1,2,3]\n        let new1: [Int] = [3]\n        let new2: [Int] = [1]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([] as [Int] == original.applying(mergeDiff))\n    }\n\n    @Test func mergeBranchEmpty() {\n        let original: [Int] = [1,2,3]\n        let new1: [Int] = []\n        let new2: [Int] = [2,3,4]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([4] == original.applying(mergeDiff))\n    }\n\n    @Test func mergeOriginalEmpty() {\n        let original: [Int] = []\n        let new1: [Int] = [1,2,3]\n        let new2: [Int] = [2,3,4]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,2,3,2,3,4] == original.applying(mergeDiff))\n    }\n\n    @Test func mergeAllEmpty() {\n        let original: [Int] = []\n        let new1: [Int] = []\n        let new2: [Int] = []\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([] as [Int] == original.applying(mergeDiff))\n    }\n\n    @Test func complexMerge() {\n        let original: [Int] = [1,2,3,4,5]\n        let new1: [Int] = [2,3,6,7]\n        let new2: [Int] = [0,2,3,4,8,9]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([0,2,3,6,7,8,9] == original.applying(mergeDiff))\n    }\n\n    @Test func deletesOverlappingInsertsMerge() {\n        let original: [Int] = [1,2,3,4,5]\n        let new1: [Int] = [1,4,5]\n        let new2: [Int] = [1,6,7,2,3,4,5]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,6,7,4,5] == original.applying(mergeDiff))\n    }\n\n    @Test func insertAtEndMerge() {\n        let original: [Int] = [1,2,3]\n        let new1: [Int] = [1,2,3,4]\n        let new2: [Int] = [1,2,3]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,2,3,4] == original.applying(mergeDiff))\n    }\n\n    @Test func deleteAndInsertAtEndMerge() {\n        let original: [Int] = [1,2,3]\n        let new1: [Int] = [1,2,3,4]\n        let new2: [Int] = [1,2]\n        let diff1 = original.diff(leadingTo: new1)\n        let diff2 = original.diff(leadingTo: new2)\n        let mergeDiff = ArrayDiff(merging: diff1, with: diff2)\n        #expect([1,2,4] == original.applying(mergeDiff))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/CloudFileSystemExchangeTests.swift",
    "content": "//\n//  CloudFileSystemExchangeTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 03/03/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n// MARK: - Mock Cloud File System\n\n/// An in-memory implementation of `CloudFileSystem` for testing.\nfinal class MockCloudFileSystem: CloudFileSystem, @unchecked Sendable {\n\n    private var files: [String: Data] = [:]\n\n    func fileExists(at path: String) async throws -> Bool {\n        files[path] != nil\n    }\n\n    func contentsOfDirectory(at path: String) async throws -> [String] {\n        let prefix = path.hasSuffix(\"/\") ? path : path + \"/\"\n        var names: Set<String> = []\n        for key in files.keys {\n            if key.hasPrefix(prefix) {\n                let remainder = String(key.dropFirst(prefix.count))\n                // Only direct children (no further slashes)\n                if !remainder.contains(\"/\") {\n                    names.insert(remainder)\n                }\n            }\n        }\n        return Array(names).sorted()\n    }\n\n    func upload(data: Data, to path: String) async throws {\n        files[path] = data\n    }\n\n    func download(from path: String) async throws -> Data {\n        guard let data = files[path] else {\n            throw CloudFileSystemError.fileNotFound\n        }\n        return data\n    }\n\n    func remove(at path: String) async throws {\n        files.removeValue(forKey: path)\n    }\n\n    func removeDirectory(at path: String) async throws {\n        let prefix = path.hasSuffix(\"/\") ? path : path + \"/\"\n        files = files.filter { !$0.key.hasPrefix(prefix) && $0.key != path }\n    }\n\n    /// Expose stored file paths for assertions.\n    var storedPaths: [String] {\n        Array(files.keys).sorted()\n    }\n}\n\n// MARK: - Tests\n\n@Suite class CloudFileSystemExchangeTests {\n\n    let store1: Store\n    let store2: Store\n    let rootURL1: URL\n    let rootURL2: URL\n    let mockFS: MockCloudFileSystem\n    let exchange1: CloudFileSystemExchange\n    let exchange2: CloudFileSystemExchange\n\n    init() throws {\n        rootURL1 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL1)\n        store2 = try Store(rootDirectoryURL: rootURL2)\n        mockFS = MockCloudFileSystem()\n        exchange1 = CloudFileSystemExchange(cloudFileSystem: mockFS, store: store1)\n        exchange2 = CloudFileSystemExchange(cloudFileSystem: mockFS, store: store2)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL1)\n        try? FileManager.default.removeItem(at: rootURL2)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    // MARK: - Exchange Tests\n\n    @Test func sendFiles() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        // Nothing in the cloud yet\n        let pathsBefore = mockFS.storedPaths\n        #expect(pathsBefore.isEmpty)\n\n        let versionIds = try await exchange1.send()\n        #expect(versionIds.contains(ver.id))\n\n        // Verify files were uploaded\n        let pathsAfter = mockFS.storedPaths\n        #expect(pathsAfter.contains { $0.contains(\"versions/\\(ver.id.rawValue)\") })\n        #expect(pathsAfter.contains { $0.contains(\"changes/\\(ver.id.rawValue)\") })\n    }\n\n    @Test func receiveFiles() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let _ = try await exchange1.send()\n        let versionIds = try await exchange2.retrieve()\n\n        #expect(versionIds.contains(ver.id))\n        #expect(try ver == store2.version(identifiedBy: ver.id))\n        #expect(try store2.value(id: val.id, at: ver.id) != nil)\n    }\n\n    @Test func concurrentChanges() async throws {\n        let origin = try store1.makeVersion(basedOnPredecessor: nil, storing: [])\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n\n        func add(numberOfVersions: Int, store: Store) -> ([Version], [Value]) {\n            var versions: [Version] = []\n            var values: [Value] = []\n            for _ in 0..<numberOfVersions {\n                let id = UUID().uuidString\n                let val = value(id, stringData: id)\n                let ver = try! store.makeVersion(basedOnPredecessor: versions.last?.id ?? origin.id, storing: [.insert(val)])\n                versions.append(ver)\n                values.append(val)\n            }\n            return (versions, values)\n        }\n\n        let (versions1, values1) = add(numberOfVersions: 3, store: store1)\n        let (versions2, values2) = add(numberOfVersions: 3, store: store2)\n\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n        let _ = try await exchange2.send()\n        let _ = try await exchange1.retrieve()\n\n        versions1.forEach { #expect(try! store2.version(identifiedBy: $0.id) != nil) }\n        versions2.forEach { #expect(try! store1.version(identifiedBy: $0.id) != nil) }\n\n        for (ver, val) in zip(versions1, values1) {\n            let val2 = try store2.value(id: val.id, storedAt: ver.id)!\n            #expect(val.data == val2.data)\n        }\n        for (ver, val) in zip(versions2, values2) {\n            let val1 = try store1.value(id: val.id, storedAt: ver.id)!\n            #expect(val.data == val1.data)\n        }\n\n        let merge = try store1.mergeRelated(version: versions1.last!.id, with: versions2.last!.id, resolvingWith: MostRecentBranchFavoringArbiter())\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n        #expect(try store2.version(identifiedBy: merge.id) != nil)\n        for val in values1 + values2 {\n            let val2 = try store2.value(id: val.id, at: merge.id)!\n            #expect(val.data == val2.data)\n        }\n    }\n\n    @Test func snapshotRoundTrip() async throws {\n        let manifest = SnapshotManifest(\n            format: \"test\",\n            latestVersionId: Version.ID(UUID().uuidString),\n            versionCount: 5,\n            chunkCount: 3,\n            totalSize: 300\n        )\n        let chunks = (0..<3).map { \"chunk-\\($0)-data\".data(using: .utf8)! }\n\n        try await exchange1.sendSnapshot(manifest: manifest) { index in\n            chunks[index]\n        }\n\n        // Retrieve manifest\n        let retrieved = try await exchange2.retrieveSnapshotManifest()\n        #expect(retrieved != nil)\n        #expect(retrieved?.chunkCount == 3)\n        #expect(retrieved?.latestVersionId == manifest.latestVersionId)\n\n        // Retrieve chunks\n        for i in 0..<3 {\n            let chunkData = try await exchange2.retrieveSnapshotChunk(index: i)\n            #expect(chunkData == chunks[i])\n        }\n    }\n\n    @Test func basePathIsolation() async throws {\n        let mockFS2 = MockCloudFileSystem()\n        let exchangeA = CloudFileSystemExchange(cloudFileSystem: mockFS2, store: store1, basePath: \"storeA\")\n        let exchangeB = CloudFileSystemExchange(cloudFileSystem: mockFS2, store: store2, basePath: \"storeB\")\n\n        let valA = value(\"A\", stringData: \"DataA\")\n        let _ = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(valA)])\n\n        let valB = value(\"B\", stringData: \"DataB\")\n        let _ = try store2.makeVersion(basedOnPredecessor: nil, storing: [.insert(valB)])\n\n        let _ = try await exchangeA.send()\n        let _ = try await exchangeB.send()\n\n        // Verify files are under different paths\n        let pathsA = mockFS2.storedPaths.filter { $0.hasPrefix(\"storeA/\") }\n        let pathsB = mockFS2.storedPaths.filter { $0.hasPrefix(\"storeB/\") }\n        #expect(!pathsA.isEmpty)\n        #expect(!pathsB.isEmpty)\n\n        // Verify no overlap\n        let overlapPaths = Set(pathsA).intersection(Set(pathsB))\n        #expect(overlapPaths.isEmpty)\n\n        // Store2 should not see storeA's versions via exchangeB\n        let idsFromB = try await exchangeB.retrieveAllVersionIdentifiers()\n        #expect(idsFromB.count == 1) // Only storeB's version\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/DataCompressionTests.swift",
    "content": "//\n//  DataCompressionTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite struct DataCompressionTests {\n\n    @Test func roundTripJSON() {\n        // Repetitive JSON compresses well and exceeds the minimum size threshold\n        let json = \"\"\"\n        {\"rawValue\":\"550e8400-e29b-41d4-a716-446655440000\",\"storedVersionId\":\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\"valueReference\":{\"valueId\":\"test-value\",\"storedVersionId\":\"550e8400-e29b-41d4-a716-446655440000\"}}\n        \"\"\"\n        let original = json.data(using: .utf8)!\n        let compressed = DataCompression.compress(original)\n        #expect(compressed.count < original.count)\n        let decompressed = DataCompression.decompressIfNeeded(compressed)\n        #expect(decompressed == original)\n    }\n\n    @Test func roundTripBinaryData() {\n        // Large binary data with some repetition\n        var data = Data(count: 1000)\n        for i in 0..<data.count {\n            data[i] = UInt8(i % 10)\n        }\n        let compressed = DataCompression.compress(data)\n        #expect(compressed.count < data.count)\n        let decompressed = DataCompression.decompressIfNeeded(compressed)\n        #expect(decompressed == data)\n    }\n\n    @Test func smallDataNotCompressed() {\n        let small = Data([0x01, 0x02, 0x03, 0x04])\n        let result = DataCompression.compress(small)\n        #expect(result == small, \"Data below threshold should not be compressed\")\n    }\n\n    @Test func emptyData() {\n        let empty = Data()\n        #expect(DataCompression.compress(empty) == empty)\n        #expect(DataCompression.decompressIfNeeded(empty) == empty)\n    }\n\n    @Test func legacyUncompressedJSONPassesThrough() {\n        let json = \"{\\\"key\\\":\\\"value\\\"}\".data(using: .utf8)!\n        let result = DataCompression.decompressIfNeeded(json)\n        #expect(result == json, \"Uncompressed JSON should pass through unchanged\")\n    }\n\n    @Test func legacyUncompressedArrayPassesThrough() {\n        let json = \"[1,2,3]\".data(using: .utf8)!\n        let result = DataCompression.decompressIfNeeded(json)\n        #expect(result == json, \"Uncompressed JSON array should pass through unchanged\")\n    }\n\n    @Test func legacyBinaryDataPassesThrough() {\n        // Binary data that doesn't start with LLZF magic\n        let binary = Data([0xFF, 0xFE, 0xFD, 0xFC, 0xFB])\n        let result = DataCompression.decompressIfNeeded(binary)\n        #expect(result == binary, \"Non-compressed binary data should pass through unchanged\")\n    }\n\n    @Test func incompressibleDataNotCompressed() {\n        // Random data that doesn't compress well\n        var random = Data(count: 200)\n        for i in 0..<random.count {\n            random[i] = UInt8.random(in: 0...255)\n        }\n        let result = DataCompression.compress(random)\n        // Either returns original (compression didn't help) or compressed version\n        let decompressed = DataCompression.decompressIfNeeded(result)\n        #expect(decompressed == random, \"Round-trip should preserve data even if compression didn't help\")\n    }\n\n    @Test func compressedDataHasMagicPrefix() {\n        let json = String(repeating: \"{\\\"key\\\":\\\"value\\\"},\", count: 20).data(using: .utf8)!\n        let compressed = DataCompression.compress(json)\n        #expect(compressed != json, \"Repetitive data should be compressed\")\n        // Check LLZF magic prefix\n        #expect(compressed[0] == 0x4C) // 'L'\n        #expect(compressed[1] == 0x4C) // 'L'\n        #expect(compressed[2] == 0x5A) // 'Z'\n        #expect(compressed[3] == 0x46) // 'F'\n    }\n\n    @Test func largeDataRoundTrip() {\n        // Simulate a realistic map node with repeated UUID patterns\n        var parts: [String] = []\n        for _ in 0..<50 {\n            parts.append(\"\\\"\\(UUID().uuidString)\\\":{\\\"storedVersionId\\\":\\\"\\(UUID().uuidString)\\\"}\")\n        }\n        let json = \"{\\(parts.joined(separator: \",\"))}\".data(using: .utf8)!\n        let compressed = DataCompression.compress(json)\n        #expect(compressed.count < json.count)\n        let decompressed = DataCompression.decompressIfNeeded(compressed)\n        #expect(decompressed == json)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/DiffTests.swift",
    "content": "//\n//  DiffTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 11/01/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class DiffTests {\n\n    let fm = FileManager.default\n\n    let zone: Zone\n    let rootURL: URL\n    let map: Map\n\n    func add(values: [String], version: String, basedOn: String?) {\n        let basedOnVersionId = basedOn.flatMap { Version.ID($0) }\n        let versionId = Version.ID(version)\n        var deltas: [Map.Delta] = []\n        for valueKey in values {\n            let valueRef = Value.Reference(valueId: .init(valueKey), storedVersionId: versionId)\n            var delta: Map.Delta = .init(key: .init(valueKey))\n            delta.addedValueReferences = [valueRef]\n            deltas.append(delta)\n        }\n        try! map.addVersion(versionId, basedOn: basedOnVersionId, applying: deltas)\n    }\n\n    func remove(values: [String], version: String, basedOn: String?) {\n        let basedOnVersionId = basedOn.flatMap { Version.ID($0) }\n        let versionId = Version.ID(version)\n        var deltas: [Map.Delta] = []\n        for valueKey in values {\n            var delta: Map.Delta = .init(key: .init(valueKey))\n            delta.removedValueIdentifiers = [.init(valueKey)]\n            deltas.append(delta)\n        }\n        try! map.addVersion(versionId, basedOn: basedOnVersionId, applying: deltas)\n    }\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        zone = FileZone(rootDirectory: rootURL, fileExtension: \".txt\")\n        map = Map(zone: zone)\n    }\n\n    deinit {\n        try? fm.removeItem(at: rootURL)\n    }\n\n    @Test func disjointInserts() throws {\n        add(values: [\"AB0000\"], version: \"0000\", basedOn: nil)\n        add(values: [\"AB1111\", \"AB1155\", \"CD1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"AB2222\", \"AB1166\", \"CD2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.count == 6)\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.inserted(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1155\" && $0.valueFork == Value.Fork.inserted(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"CD1111\" && $0.valueFork == Value.Fork.inserted(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB2222\" && $0.valueFork == Value.Fork.inserted(.second) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1166\" && $0.valueFork == Value.Fork.inserted(.second) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"CD2222\" && $0.valueFork == Value.Fork.inserted(.second) }))\n        #expect(!diffs.contains(where: { $0.valueId.rawValue == \"CD2222\" && $0.valueFork == Value.Fork.inserted(.first) }))\n    }\n\n    @Test func inserts() throws {\n        add(values: [], version: \"0000\", basedOn: nil)\n        add(values: [\"AB1111\", \"MM1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"AB1111\", \"AB1112\", \"ZZ2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.count == 4)\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.twiceInserted }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"MM1111\" && $0.valueFork == Value.Fork.inserted(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"ZZ2222\" && $0.valueFork == Value.Fork.inserted(.second) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1112\" && $0.valueFork == Value.Fork.inserted(.second) }))\n    }\n\n    @Test func updates() throws {\n        add(values: [\"AB1111\", \"MM1111\"], version: \"0000\", basedOn: nil)\n        add(values: [\"AB1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"AB1111\", \"MM1111\", \"ZZ2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.count == 3)\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.twiceUpdated }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"MM1111\" && $0.valueFork == Value.Fork.updated(.second) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"ZZ2222\" && $0.valueFork == Value.Fork.inserted(.second) }))\n    }\n\n    @Test func removes() throws {\n        add(values: [\"AB1111\", \"MM1111\", \"ZZ2222\"], version: \"0000\", basedOn: nil)\n        remove(values: [\"AB1111\", \"MM1111\"], version: \"1111\", basedOn: \"0000\")\n        remove(values: [\"AB1111\", \"ZZ2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.count == 3)\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.twiceRemoved }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"MM1111\" && $0.valueFork == Value.Fork.removed(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"ZZ2222\" && $0.valueFork == Value.Fork.removed(.second) }))\n    }\n\n    @Test func updateRemove() throws {\n        add(values: [\"AB1111\"], version: \"0000\", basedOn: nil)\n        remove(values: [\"AB1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"AB1111\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.count == 1)\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.removedAndUpdated(removedOn: .first) }))\n    }\n\n    @Test func unchangedBucketsProduceNoDiffs() throws {\n        add(values: [\"AB0000\", \"ZZ0000\"], version: \"0000\", basedOn: nil)\n        add(values: [\"AB1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"AB2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        // Only \"AB\" bucket changed; \"ZZ\" bucket is identical in both branches\n        #expect(diffs.allSatisfy({ $0.valueId.rawValue.hasPrefix(\"AB\") }))\n        #expect(!diffs.contains(where: { $0.valueId.rawValue == \"ZZ0000\" }))\n    }\n\n    @Test func oneBranchUnchangedBucket() throws {\n        add(values: [\"AB0000\", \"ZZ0000\"], version: \"0000\", basedOn: nil)\n        add(values: [\"AB1111\"], version: \"1111\", basedOn: \"0000\")\n        add(values: [\"ZZ2222\"], version: \"2222\", basedOn: \"0000\")\n        let diffs = try map.differences(between: .init(\"1111\"), and: .init(\"2222\"), withCommonAncestor: .init(\"0000\"))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"AB1111\" && $0.valueFork == Value.Fork.inserted(.first) }))\n        #expect(diffs.contains(where: { $0.valueId.rawValue == \"ZZ2222\" && $0.valueFork == Value.Fork.inserted(.second) }))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/DynamicTaskBatcherTests.swift",
    "content": "//\n//  DynamicTaskBatcherTests.swift\n//\n//\n//  Created by Drew McCormack on 06/03/2020.\n//\n\nimport Foundation\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite struct DynamicTaskBatcherTests {\n\n    enum TestError: Swift.Error {\n        case testError\n    }\n\n    @Test func failure() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 10, taskCostEvaluator: { _ in 0.1 }) { range in\n            count += 1\n            return .definitive(.failure(TestError.testError))\n        }\n\n        do {\n            try await batcher.start()\n            Issue.record(\"Should have thrown\")\n        } catch {\n            // Expected\n        }\n        #expect(count == 1)\n    }\n\n    @Test func zeroTasks() async throws {\n        let batcher = DynamicTaskBatcher(numberOfTasks: 0, taskCostEvaluator: { _ in 0.1 }) { _ in\n            return .definitive(.success(()))\n        }\n        try await batcher.start()\n    }\n\n    @Test func oneTask() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 1, taskCostEvaluator: { _ in 0.1 }) { range in\n            count += 1\n            #expect(range == 0..<1)\n            return .definitive(.success(()))\n        }\n\n        try await batcher.start()\n        #expect(count == 1)\n    }\n\n    @Test func oneLargeTask() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 1, taskCostEvaluator: { _ in 2.0 }) { range in\n            count += 1\n            #expect(range == 0..<1)\n            return .definitive(.success(()))\n        }\n\n        try await batcher.start()\n        #expect(count == 1)\n    }\n\n    @Test func twoSmallTasks() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 2, taskCostEvaluator: { _ in 0.1 }) { range in\n            count += 1\n            #expect(range == 0..<2)\n            return .definitive(.success(()))\n        }\n\n        try await batcher.start()\n        #expect(count == 1)\n    }\n\n    @Test func twoLargeTasks() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 2, taskCostEvaluator: { _ in 1.0 }) { range in\n            count += 1\n            #expect(range.count == 1)\n            return .definitive(.success(()))\n        }\n\n        try await batcher.start()\n        #expect(count == 2)\n    }\n\n    @Test func accumulatingCost() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 4, taskCostEvaluator: { index in\n            switch index {\n            case 0, 1:\n                return 0.5\n            case 2:\n                return 0.49\n            case 3:\n                return 0.02\n            default:\n                return 0.1\n            }\n        }) { range in\n            count += 1\n            if range.lowerBound == 0 {\n                #expect(range.count == 1)\n            } else if range.lowerBound == 1 {\n                #expect(range.count == 2)\n            } else {\n                #expect(range.count == 1)\n            }\n            return .definitive(.success(()))\n        }\n\n        try await batcher.start()\n        #expect(count == 3)\n    }\n\n    @Test func growingAndRepeatingBatchesUntilFail() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 2, taskCostEvaluator: { _ in 1.01 }) { range in\n            count += 1\n            #expect(range.lowerBound == 0)\n            return .growBatchAndReexecute\n        }\n\n        do {\n            try await batcher.start()\n            Issue.record(\"Should have thrown\")\n        } catch {\n            // Expected\n        }\n        #expect(count == 2)\n    }\n\n    @Test func growingAndRepeatingBatchesWithSuccess() async throws {\n        var count = 0\n        let batcher = DynamicTaskBatcher(numberOfTasks: 3, taskCostEvaluator: { _ in 1.01 }) { range in\n            count += 1\n            switch range {\n            case 0..<1:\n                return .growBatchAndReexecute\n            case 0..<2:\n                return .definitive(.success(()))\n            case 2..<3:\n                return .definitive(.success(()))\n            default:\n                Issue.record()\n                return .definitive(.failure(TestError.testError))\n            }\n        }\n\n        try await batcher.start()\n        #expect(count == 3)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/FileSystemExchangeTests.swift",
    "content": "//\n//  FileSystemExchangeTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 08/03/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class FileSystemExchangeTests {\n\n    let fm = FileManager.default\n\n    let store1: Store\n    let store2: Store\n    let rootURL1: URL\n    let rootURL2: URL\n    let exchangeURL: URL\n    var exchange1: FileSystemExchange\n    var exchange2: FileSystemExchange\n\n    let recentChangeArbiter: MostRecentChangeFavoringArbiter\n\n    init() throws {\n        rootURL1 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL1)\n        store2 = try Store(rootDirectoryURL: rootURL2)\n        recentChangeArbiter = MostRecentChangeFavoringArbiter()\n        exchangeURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        exchange1 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store1, usesFileCoordination: false)\n        exchange2 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: false)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL1)\n        try? FileManager.default.removeItem(at: rootURL2)\n        try? FileManager.default.removeItem(at: exchangeURL)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    private var changeFiles: [URL] {\n        return try! fm.contentsOfDirectory(at: exchangeURL.appendingPathComponent(\"changes\"), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])\n    }\n\n    private var versionFiles: [URL] {\n        return try! fm.contentsOfDirectory(at: exchangeURL.appendingPathComponent(\"versions\"), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])\n    }\n\n    @Test func sendFiles() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n        #expect(changeFiles.count == 0)\n        #expect(versionFiles.count == 0)\n        let versionIds = try await exchange1.send()\n        #expect(versionIds.contains(ver.id))\n        #expect(self.changeFiles.count == 1)\n        #expect(self.versionFiles.count == 1)\n    }\n\n    @Test func receiveFiles() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n        let _ = try await exchange1.send()\n        let versionIds = try await exchange2.retrieve()\n        #expect(versionIds.contains(ver.id))\n        #expect(try ver == self.store2.version(identifiedBy: ver.id))\n        #expect(try self.store2.value(id: val.id, at: ver.id) != nil)\n    }\n\n    @Test func concurrentChanges() async throws {\n        let origin = try store1.makeVersion(basedOnPredecessor: nil, storing: [])\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n\n        func add(numberOfVersions: Int, store: Store) -> ([Version], [Value]) {\n            var versions: [Version] = []\n            var values: [Value] = []\n            for _ in 0..<numberOfVersions {\n                let id = UUID().uuidString\n                let val = value(id, stringData: id)\n                let ver = try! store.makeVersion(basedOnPredecessor: versions.last?.id ?? origin.id, storing: [.insert(val)])\n                versions.append(ver)\n                values.append(val)\n            }\n            return (versions, values)\n        }\n\n        let (versions1, values1) = add(numberOfVersions: 3, store: store1)\n        let (versions2, values2) = add(numberOfVersions: 3, store: store2)\n\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n        let _ = try await exchange2.send()\n        let _ = try await exchange1.retrieve()\n\n        versions1.forEach { #expect(try! self.store2.version(identifiedBy: $0.id) != nil) }\n        versions2.forEach { #expect(try! self.store1.version(identifiedBy: $0.id) != nil) }\n        for (ver, val) in zip(versions1, values1) {\n            let val2 = try self.store2.value(id: val.id, storedAt: ver.id)!\n            #expect(val.data == val2.data)\n        }\n        for (ver, val) in zip(versions2, values2) {\n            let val1 = try self.store1.value(id: val.id, storedAt: ver.id)!\n            #expect(val.data == val1.data)\n        }\n\n        let merge = try store1.mergeRelated(version: versions1.last!.id, with: versions2.last!.id, resolvingWith: MostRecentBranchFavoringArbiter())\n        let _ = try await exchange1.send()\n        let _ = try await exchange2.retrieve()\n        #expect(try self.store2.version(identifiedBy: merge.id) != nil)\n        for val in values1 + values2 {\n            let val2 = try self.store2.value(id: val.id, at: merge.id)!\n            #expect(val.data == val2.data)\n        }\n    }\n\n    @Test func newVersionAvailableNotification() async throws {\n        exchange1 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store1, usesFileCoordination: true)\n        exchange2 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: true)\n\n        let _ = try store1.makeVersion(basedOnPredecessor: nil, storing: [])\n\n        // Start listening before the send\n        let notificationTask = Task {\n            for await _ in exchange2.newVersionsAvailable {\n                return // Got a notification\n            }\n        }\n\n        let _ = try await exchange1.send()\n\n        // Wait for the notification with a timeout\n        let timeoutTask = Task {\n            try await Task.sleep(for: .seconds(3))\n        }\n\n        // Race: either we get the notification or we time out\n        await withTaskGroup(of: Bool.self) { group in\n            group.addTask { await notificationTask.value; return true }\n            group.addTask { try? await timeoutTask.value; return false }\n            let result = await group.next()!\n            group.cancelAll()\n            if !result {\n                Issue.record(\"Timed out waiting for newVersionsAvailable notification\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/FileZoneTests.swift",
    "content": "//\n//  FileZoneTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 07/12/2018.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class FileZoneTests {\n\n    let fm = FileManager.default\n\n    let zone: FileZone\n    let rootURL: URL\n    let ref: ZoneReference\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        zone = FileZone(rootDirectory: rootURL, fileExtension: \"json\")\n        ref = ZoneReference(key: \"ABCDEF\", version: .init(\"1234\"))\n    }\n\n    deinit {\n        try? fm.removeItem(at: rootURL)\n    }\n\n    @Test func creation() {\n        #expect(fm.fileExists(atPath: rootURL.path))\n    }\n\n    @Test func addingDataCreatesFiles() throws {\n        try zone.store(Data(), for: ref)\n        fm.fileExists(atPath: rootURL.appendingPathComponent(\"AB/CDEF/1/234.json\").path)\n    }\n\n    @Test func addingMultipleReferencesInSameDiretories() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: \"ABCDEF\", version: .init(\"1245\")))\n        fm.fileExists(atPath: rootURL.appendingPathComponent(\"AB/CDEF/1/245.json\").path)\n    }\n\n    @Test func addingMultipleReferencesWithDifferentVersionDirectories() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: \"ABCDEF\", version: .init(\"2222\")))\n        fm.fileExists(atPath: rootURL.appendingPathComponent(\"AB/CDEF/2/222.json\").path)\n    }\n\n    @Test func retrievingNonExistentData() throws {\n        let data = try zone.data(for: ref)\n        #expect(data == nil)\n    }\n\n    @Test func retrievingData() throws {\n        try zone.store(\"Test\".data(using: .utf8)!, for: ref)\n        let data = try zone.data(for: ref)\n        #expect(data != nil)\n        let string = String(bytes: data!, encoding: .utf8)\n        #expect(string == \"Test\")\n    }\n\n    @Test func versionsQuery() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: \"ABCDEF\", version: .init(\"1245\")))\n        let versions = try zone.versionIds(for: \"ABCDEF\")\n        let versionStrings = versions.map { $0.rawValue }\n        #expect(versions.count == 2)\n        #expect(versionStrings.contains(\"1234\"))\n        #expect(versionStrings.contains(\"1245\"))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/GeneralTests.swift",
    "content": "//\n//  File.swift\n//\n//\n//  Created by Drew McCormack on 15/09/2019.\n//\n\nimport Foundation\nimport Testing\n@testable import LLVS\n\n@Suite struct GeneralTests {\n\n    @Test func rangeSplitting() {\n        #expect((5...10).split(intoRangesOfLength: 2) == [5...6, 7...8, 9...10])\n        #expect((5...10).split(intoRangesOfLength: 3) == [5...7, 8...10])\n        #expect((5...10).split(intoRangesOfLength: 4) == [5...8, 9...10])\n        #expect((5...10).split(intoRangesOfLength: 5) == [5...9, 10...10])\n        #expect((5...10).split(intoRangesOfLength: 6) == [5...10])\n        #expect((5...10).split(intoRangesOfLength: 7) == [5...10])\n        #expect((5...5).split(intoRangesOfLength: 2) == [5...5])\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/HistoryTests.swift",
    "content": "//\n//  HistoryTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 12/11/2018.\n//\n\nimport Testing\n@testable import LLVS\n\n@Suite struct HistoryTests {\n\n    var history: History\n\n    init() {\n        history = History()\n    }\n\n    @Test func emptyHistory() {\n        #expect(history.headIdentifiers.isEmpty)\n        #expect(history.mostRecentHead == nil)\n\n        let versions: (Version.ID, Version.ID) = (.init(\"ABCD\"), .init(\"CDEF\"))\n        #expect(throws: (any Error).self) { try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions) }\n    }\n\n    @Test mutating func singleVersion() throws {\n        let version = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version, updatingPredecessorVersions: true)\n        #expect(history.headIdentifiers.count == 1)\n        #expect(history.headIdentifiers.first?.rawValue == \"ABCD\")\n        #expect(history.mostRecentHead?.id.rawValue == \"ABCD\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"ABCD\"), .init(\"CDEF\"))\n        #expect(throws: (any Error).self) { try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions) }\n    }\n\n    @Test mutating func addingVersionTwice() throws {\n        let version = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version, updatingPredecessorVersions: true)\n        #expect(throws: (any Error).self) { try history.add(version, updatingPredecessorVersions: true) }\n    }\n\n    @Test mutating func unrelatedVersions() throws {\n        let version1 = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version1, updatingPredecessorVersions: true)\n\n        let version2 = Version(id: .init(\"CDEF\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version2, updatingPredecessorVersions: true)\n\n        let sortedHeads = history.headIdentifiers.sorted { $0.rawValue < $1.rawValue }\n        #expect(sortedHeads.count == 2)\n        #expect(sortedHeads.first?.rawValue == \"ABCD\")\n        #expect(sortedHeads.last?.rawValue == \"CDEF\")\n        #expect(history.mostRecentHead?.id.rawValue == \"CDEF\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"ABCD\"), .init(\"CDEF\"))\n        #expect(try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions) == nil)\n    }\n\n    @Test mutating func simpleSerialHistory() throws {\n        let version1 = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version1, updatingPredecessorVersions: true)\n\n        let predecessors = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version2 = Version(id: .init(\"CDEF\"), predecessors: predecessors, valueDataSize: 0)\n        try history.add(version2, updatingPredecessorVersions: true)\n\n        let sortedHeads = history.headIdentifiers.sorted { $0.rawValue < $1.rawValue }\n        #expect(sortedHeads.count == 1)\n        #expect(sortedHeads.first?.rawValue == \"CDEF\")\n        #expect(history.mostRecentHead?.id.rawValue == \"CDEF\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"ABCD\"), .init(\"CDEF\"))\n        let common = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions)\n        #expect(common == version1.id)\n    }\n\n    @Test mutating func serialHistory() throws {\n        let version1 = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version1, updatingPredecessorVersions: true)\n\n        let predecessors2 = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version2 = Version(id: .init(\"CDEF\"), predecessors: predecessors2, valueDataSize: 0)\n        try history.add(version2, updatingPredecessorVersions: true)\n\n        let predecessors3 = Version.Predecessors(idOfFirst: version2.id, idOfSecond: nil)\n        let version3 = Version(id: .init(\"GHIJ\"), predecessors: predecessors3, valueDataSize: 50000000)\n        try history.add(version3, updatingPredecessorVersions: true)\n\n        let sortedHeads = history.headIdentifiers.sorted { $0.rawValue < $1.rawValue }\n        #expect(sortedHeads.count == 1)\n        #expect(sortedHeads.first?.rawValue == \"GHIJ\")\n        #expect(history.mostRecentHead?.id.rawValue == \"GHIJ\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"ABCD\"), .init(\"GHIJ\"))\n        let common = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions)\n        #expect(common == version1.id)\n    }\n\n    @Test mutating func branch() throws {\n        let version1 = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version1, updatingPredecessorVersions: true)\n\n        let predecessors2 = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version2 = Version(id: .init(\"CDEF\"), predecessors: predecessors2, valueDataSize: 0)\n        try history.add(version2, updatingPredecessorVersions: true)\n\n        let predecessors3 = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version3 = Version(id: .init(\"GHIJ\"), predecessors: predecessors3, valueDataSize: 0)\n        try history.add(version3, updatingPredecessorVersions: true)\n\n        let sortedHeads = history.headIdentifiers.sorted { $0.rawValue < $1.rawValue }\n        #expect(sortedHeads.count == 2)\n        #expect(sortedHeads.first?.rawValue == \"CDEF\")\n        #expect(history.mostRecentHead?.id.rawValue == \"GHIJ\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"CDEF\"), .init(\"GHIJ\"))\n        let common = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions)\n        #expect(common == version1.id)\n    }\n\n    @Test mutating func branchAndMerge() throws {\n        let version1 = Version(id: .init(\"ABCD\"), predecessors: nil, valueDataSize: 0)\n        try history.add(version1, updatingPredecessorVersions: true)\n\n        let predecessors2 = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version2 = Version(id: .init(\"CDEF\"), predecessors: predecessors2, valueDataSize: 50000000)\n        try history.add(version2, updatingPredecessorVersions: true)\n\n        let predecessors3 = Version.Predecessors(idOfFirst: version1.id, idOfSecond: nil)\n        let version3 = Version(id: .init(\"GHIJ\"), predecessors: predecessors3, valueDataSize: 50000000)\n        try history.add(version3, updatingPredecessorVersions: true)\n\n        let predecessors4 = Version.Predecessors(idOfFirst: version2.id, idOfSecond: version3.id)\n        let version4 = Version(id: .init(\"KLMN\"), predecessors: predecessors4, valueDataSize: 50000000)\n        try history.add(version4, updatingPredecessorVersions: true)\n\n        let sortedHeads = history.headIdentifiers.sorted { $0.rawValue < $1.rawValue }\n        #expect(sortedHeads.count == 1)\n        #expect(sortedHeads.first?.rawValue == \"KLMN\")\n        #expect(history.mostRecentHead?.id.rawValue == \"KLMN\")\n\n        let versions: (Version.ID, Version.ID) = (.init(\"KLMN\"), .init(\"GHIJ\"))\n        let common = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: versions)\n        #expect(common == version3.id)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MapTests.swift",
    "content": "//\n//  MapTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 09/12/2018.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class MapTests {\n\n    let fm = FileManager.default\n\n    let zone: Zone\n    let rootURL: URL\n    let map: Map\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        zone = FileZone(rootDirectory: rootURL, fileExtension: \".txt\")\n        map = Map(zone: zone)\n    }\n\n    deinit {\n        try? fm.removeItem(at: rootURL)\n    }\n\n    @Test func firstCommit() throws {\n        let valueKey = \"ABCD\"\n        let versionId = Version.ID(\"1234\")\n        let valueRef = Value.Reference(valueId: .init(valueKey), storedVersionId: versionId)\n        var delta: Map.Delta = .init(key: .init(valueKey))\n        delta.addedValueReferences = [valueRef]\n        try map.addVersion(versionId, basedOn: nil, applying: [delta])\n        let valueRefs = try map.valueReferences(matching: .init(valueKey), at: versionId)\n        #expect(valueRefs.count == 1)\n        #expect(valueRefs.first! == Value.Reference(valueId: .init(valueKey), storedVersionId: versionId))\n    }\n\n    @Test func fetchingValueFromEarlierCommit() throws {\n        let valueKey = \"ABCD\"\n        var delta: Map.Delta = .init(key: .init(valueKey))\n        let versionId = Version.ID(\"1234\")\n        let valueRef = Value.Reference(valueId: .init(valueKey), storedVersionId: versionId)\n        delta.addedValueReferences = [valueRef]\n        try map.addVersion(.init(\"1234\"), basedOn: nil, applying: [delta])\n        try map.addVersion(.init(\"2345\"), basedOn: .init(\"1234\"), applying: [])\n        let valueRefs = try map.valueReferences(matching: .init(valueKey), at: .init(\"2345\"))\n        #expect(valueRefs.count == 1)\n        #expect(valueRefs.first! == Value.Reference(valueId: .init(valueKey), storedVersionId: .init(\"1234\")))\n    }\n\n    @Test func removingValue() throws {\n        let valueKey1 = \"ABCD\"\n        var delta1: Map.Delta = .init(key: .init(valueKey1))\n        let versionId1 = Version.ID(\"1234\")\n        let valueRef1 = Value.Reference(valueId: .init(valueKey1), storedVersionId: versionId1)\n        delta1.addedValueReferences = [valueRef1]\n        try map.addVersion(versionId1, basedOn: nil, applying: [delta1])\n\n        let valueKey2 = \"BCDE\"\n        let versionId2 = Version.ID(\"2345\")\n        var delta21: Map.Delta = .init(key: .init(valueKey1))\n        delta21.removedValueIdentifiers = [.init(valueKey1)]\n        var delta22: Map.Delta = .init(key: .init(valueKey2))\n        let valueRef2 = Value.Reference(valueId: .init(valueKey2), storedVersionId: versionId2)\n        delta22.addedValueReferences = [valueRef2]\n        try map.addVersion(.init(\"2345\"), basedOn: .init(\"1234\"), applying: [delta21, delta22])\n\n        var valueRefs = try map.valueReferences(matching: .init(valueKey1), at: .init(\"2345\"))\n        #expect(valueRefs.count == 0)\n\n        valueRefs = try map.valueReferences(matching: .init(valueKey2), at: .init(\"2345\"))\n        #expect(valueRefs.count == 1)\n        #expect(valueRefs.first! == Value.Reference(valueId: .init(valueKey2), storedVersionId: .init(\"2345\")))\n    }\n\n    @Test func oneToManyMap() throws {\n        var delta1: Map.Delta = .init(key: .init(\"Amsterdam\"))\n        let valueRef1 = Value.Reference(valueId: .init(\"ABCD\"), storedVersionId: .init(\"1234\"))\n        delta1.addedValueReferences = [valueRef1]\n        try map.addVersion(valueRef1.storedVersionId, basedOn: nil, applying: [delta1])\n\n        var delta2: Map.Delta = .init(key: .init(\"Amsterdam\"))\n        let valueRef2 = Value.Reference(valueId: .init(\"CDEF\"), storedVersionId: .init(\"2345\"))\n        delta2.addedValueReferences = [valueRef2]\n        try map.addVersion(valueRef2.storedVersionId, basedOn: .init(\"1234\"), applying: [delta2])\n\n        var valueRefs = try map.valueReferences(matching: .init(\"Amsterdam\"), at: .init(\"2345\"))\n        #expect(valueRefs.count == 2)\n        #expect(valueRefs.contains(.init(valueId: .init(\"ABCD\"), storedVersionId: .init(\"1234\"))))\n        #expect(valueRefs.contains(.init(valueId: .init(\"CDEF\"), storedVersionId: .init(\"2345\"))))\n\n        var delta3: Map.Delta = .init(key: .init(\"Amsterdam\"))\n        delta3.removedValueIdentifiers = [.init(\"ABCD\")]\n        try map.addVersion(.init(\"3456\"), basedOn: .init(\"2345\"), applying: [delta3])\n\n        valueRefs = try map.valueReferences(matching: .init(\"Amsterdam\"), at: .init(\"3456\"))\n        #expect(valueRefs.count == 1)\n        #expect(valueRefs.contains(.init(valueId: .init(\"CDEF\"), storedVersionId: .init(\"2345\"))))\n    }\n\n    @Test func similarKeys() throws {\n        var delta1: Map.Delta = .init(key: .init(\"Amsterdam\"))\n        let valueRef1 = Value.Reference(valueId: .init(\"ABCD\"), storedVersionId: .init(\"1234\"))\n        delta1.addedValueReferences = [valueRef1]\n        try map.addVersion(valueRef1.storedVersionId, basedOn: nil, applying: [delta1])\n\n        var delta2: Map.Delta = .init(key: .init(\"Amsterdam1\"))\n        let valueRef2 = Value.Reference(valueId: .init(\"CDEF\"), storedVersionId: .init(\"2345\"))\n        delta2.addedValueReferences = [valueRef2]\n        try map.addVersion(valueRef2.storedVersionId, basedOn: .init(\"1234\"), applying: [delta2])\n\n        do {\n            let valueRefs = try map.valueReferences(matching: .init(\"Amsterdam\"), at: .init(\"2345\"))\n            #expect(valueRefs.count == 1)\n            #expect(valueRefs.contains(.init(valueId: .init(\"ABCD\"), storedVersionId: .init(\"1234\"))))\n        }\n\n        do {\n            let valueRefs = try map.valueReferences(matching: .init(\"Amsterdam1\"), at: .init(\"2345\"))\n            #expect(valueRefs.count == 1)\n            #expect(valueRefs.contains(.init(valueId: .init(\"CDEF\"), storedVersionId: .init(\"2345\"))))\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MemoryExchangeTests.swift",
    "content": "//\n//  MemoryExchangeTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class MemoryExchangeTests {\n\n    let fm = FileManager.default\n\n    let store1: Store\n    let store2: Store\n    let rootURL1: URL\n    let rootURL2: URL\n    let exchange1: MemoryExchange\n    let exchange2: MemoryExchange\n\n    init() throws {\n        rootURL1 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL1)\n        store2 = try Store(rootDirectoryURL: rootURL2)\n\n        exchange1 = MemoryExchange(store: store1)\n        exchange2 = MemoryExchange(store: store2)\n    }\n\n    deinit {\n        try? fm.removeItem(at: rootURL1)\n        try? fm.removeItem(at: rootURL2)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    // MARK: - Tests\n\n    @Test func sendPopulatesExchange() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let versionIds = try await exchange1.send()\n        #expect(versionIds.contains(ver.id))\n    }\n\n    @Test func retrieveFromSharedExchange() async throws {\n        let shared = MemoryExchange(store: store1)\n        let exchange2WithShared = MemoryExchange(store: store2)\n\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        // Send store1's versions into the shared exchange\n        _ = try await shared.send()\n\n        // Get version data from shared and populate exchange2WithShared\n        let ids = try await shared.retrieveAllVersionIdentifiers()\n        let versions = try await shared.retrieveVersions(identifiedBy: ids)\n        let changesByVersion = try await shared.retrieveValueChanges(forVersionsIdentifiedBy: ids)\n\n        let versionChanges: [VersionChanges] = versions.map { v in\n            (v, changesByVersion[v.id] ?? [])\n        }\n\n        try await exchange2WithShared.send(versionChanges: versionChanges)\n        let retrievedIds = try await exchange2WithShared.retrieve()\n\n        #expect(retrievedIds.contains(ver.id))\n        let retrievedVersion = try store2.version(identifiedBy: ver.id)\n        #expect(ver == retrievedVersion)\n        #expect(try store2.value(id: val.id, at: ver.id) != nil)\n    }\n\n    @Test func sendAndRetrieveMultipleVersions() async throws {\n        let val1 = value(\"AAA\", stringData: \"First\")\n        let ver1 = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val1)])\n        let val2 = value(\"BBB\", stringData: \"Second\")\n        let ver2 = try store1.makeVersion(basedOnPredecessor: ver1.id, storing: [.insert(val2)])\n\n        let versionIds = try await exchange1.send()\n        #expect(versionIds.count == 2)\n        #expect(versionIds.contains(ver1.id))\n        #expect(versionIds.contains(ver2.id))\n\n        // Verify data is in exchange\n        let ids = try await exchange1.retrieveAllVersionIdentifiers()\n        #expect(ids.count == 2)\n    }\n\n    @Test func retrieveVersionsById() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        _ = try await exchange1.send()\n\n        let versions = try await exchange1.retrieveVersions(identifiedBy: [ver.id])\n        #expect(versions.count == 1)\n        #expect(versions.first?.id == ver.id)\n    }\n\n    @Test func retrieveValueChanges() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        _ = try await exchange1.send()\n\n        let changesByVersion = try await exchange1.retrieveValueChanges(forVersionsIdentifiedBy: [ver.id])\n        #expect(changesByVersion.count == 1)\n        #expect(changesByVersion[ver.id]?.count == 1)\n    }\n\n    @Test func retrieveMissingVersionsReturnsEmpty() async throws {\n        let fakeId = Version.ID(UUID().uuidString)\n        let versions = try await exchange1.retrieveVersions(identifiedBy: [fakeId])\n        #expect(versions.count == 0)\n    }\n\n    @Test func restorationStateIsNil() {\n        #expect(exchange1.restorationState == nil)\n        exchange1.restorationState = Data([1, 2, 3])\n        #expect(exchange1.restorationState == nil)\n    }\n\n    @Test func newVersionsAvailableStream() async throws {\n        _ = try store1.makeVersion(basedOnPredecessor: nil, storing: [])\n\n        // Start a task to listen for the notification\n        let notified = Task<Bool, Never> {\n            for await _ in exchange1.newVersionsAvailable {\n                return true\n            }\n            return false\n        }\n\n        // Give the listener a moment to start\n        try await Task.sleep(nanoseconds: 50_000_000)\n\n        _ = try await exchange1.send()\n\n        // Wait briefly for the notification\n        let result = await Task {\n            try? await Task.sleep(nanoseconds: 200_000_000)\n            notified.cancel()\n            return await notified.value\n        }.value\n\n        #expect(result)\n    }\n\n    @Test func sendEmptyIsNoOp() async throws {\n        let ids = try await exchange1.send()\n        #expect(ids.count == 0)\n    }\n\n    @Test func sendIdempotent() async throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        _ = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let ids1 = try await exchange1.send()\n        #expect(ids1.count == 1)\n\n        // Second send should have nothing new to send\n        let ids2 = try await exchange1.send()\n        #expect(ids2.count == 0)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MergeTests.swift",
    "content": "//\n//  MergeTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 12/01/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class MergeTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n    let valuesURL: URL\n    let originalVersion: Version\n    var branch1: Version\n    var branch2: Version\n    let originalValue: Value\n    let newValue1: Value\n    let newValue2: Value\n\n    var valueForMerge: Value?\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        valuesURL = rootURL.appendingPathComponent(\"values\")\n        store = try Store(rootDirectoryURL: rootURL)\n\n        originalValue = Value(id: .init(\"ABCDEF\"), data: \"Bob\".data(using: .utf8)!)\n        let changes: [Value.Change] = [.insert(originalValue)]\n        originalVersion = try store.makeVersion(basedOn: nil, storing: changes)\n\n        newValue1 = Value(id: .init(\"ABCDEF\"), data: \"Tom\".data(using: .utf8)!)\n        let changes1: [Value.Change] = [.insert(newValue1)]\n        let predecessors: Version.Predecessors = .init(idOfFirst: originalVersion.id, idOfSecond: nil)\n        branch1 = try store.makeVersion(basedOn: predecessors, storing: changes1)\n\n        newValue2 = Value(id: .init(\"ABCDEF\"), data: \"Jerry\".data(using: .utf8)!)\n        let changes2: [Value.Change] = [.insert(newValue2)]\n        branch2 = try store.makeVersion(basedOn: predecessors, storing: changes2)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func unresolvedMergeFails() {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                #expect(merge.forksByValueIdentifier.count == 1)\n                return []\n            }\n        }\n        #expect(throws: (any Error).self) { try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter()) }\n    }\n\n    @Test func resolvedMergeSucceeds() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let value = Value(id: .init(\"ABCDEF\"), data: \"Jack\".data(using: .utf8)!)\n                return [.insert(value)]\n            }\n        }\n        _ = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n    }\n\n    @Test func incompletelyResolvedMergeFails() {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let value = Value(id: .init(\"CDEDEF\"), data: \"Jack\".data(using: .utf8)!)\n                return [.insert(value)]\n            }\n        }\n        #expect(throws: (any Error).self) { try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter()) }\n    }\n\n    @Test func preserve() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let fork = merge.forksByValueIdentifier[.init(\"ABCDEF\")]\n                #expect(fork == .twiceUpdated)\n                let firstValue = try! store.value(id: .init(\"ABCDEF\"), at: merge.versions.first.id)!\n                return [.preserve(firstValue.reference!)]\n            }\n        }\n        let mergeVersion = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(mergeValue.data == \"Tom\".data(using: .utf8)!)\n    }\n\n    @Test func asymmetricBranchPreserve() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let fork = merge.forksByValueIdentifier[.init(\"ABCDEF\")]\n                #expect(fork == .twiceUpdated)\n                let firstValue = try! store.value(id: .init(\"ABCDEF\"), at: merge.versions.second.id)!\n                return [.preserve(firstValue.reference!)]\n            }\n        }\n\n        let predecessors: Version.Predecessors = .init(idOfFirst: branch2.id, idOfSecond: nil)\n        let newValue = Value(id: .init(\"ABCDEF\"), data: \"Pete\".data(using: .utf8)!)\n        branch2 = try store.makeVersion(basedOn: predecessors, storing: [.update(newValue)])\n\n        let mergeVersion = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(mergeValue.data == \"Pete\".data(using: .utf8)!)\n    }\n\n    @Test func remove() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let fork = merge.forksByValueIdentifier[.init(\"ABCDEF\")]\n                #expect(fork == .removedAndUpdated(removedOn: .second))\n                return [.preserveRemoval(.init(\"ABCDEF\"))]\n            }\n        }\n\n        let predecessors: Version.Predecessors = .init(idOfFirst: branch2.id, idOfSecond: nil)\n        branch2 = try store.makeVersion(basedOn: predecessors, storing: [.remove(.init(\"ABCDEF\"))])\n\n        let mergeVersion = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)\n        #expect(mergeValue == nil)\n    }\n\n    @Test func twiceRemoved() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let fork = merge.forksByValueIdentifier[.init(\"ABCDEF\")]\n                #expect(fork == .twiceRemoved)\n                return [.preserveRemoval(.init(\"ABCDEF\"))]\n            }\n        }\n\n        let predecessors1: Version.Predecessors = .init(idOfFirst: branch1.id, idOfSecond: nil)\n        branch1 = try store.makeVersion(basedOn: predecessors1, storing: [.remove(.init(\"ABCDEF\"))])\n\n        let predecessors2: Version.Predecessors = .init(idOfFirst: branch2.id, idOfSecond: nil)\n        branch2 = try store.makeVersion(basedOn: predecessors2, storing: [.remove(.init(\"ABCDEF\"))])\n\n        let mergeVersion = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)\n        #expect(mergeValue == nil)\n    }\n\n    @Test func twiceUpdated() throws {\n        class Arbiter: MergeArbiter {\n            func changes(toResolve merge: Merge, in store: Store) -> [Value.Change] {\n                let fork = merge.forksByValueIdentifier[.init(\"ABCDEF\")]\n                #expect(fork == .twiceUpdated)\n                let secondValue = try! store.value(id: .init(\"ABCDEF\"), at: merge.versions.second.id)!\n                return [.preserve(secondValue.reference!)]\n            }\n        }\n\n        let predecessors1: Version.Predecessors = .init(idOfFirst: branch1.id, idOfSecond: nil)\n        let newValue1 = Value(id: .init(\"ABCDEF\"), data: \"Pete\".data(using: .utf8)!)\n        branch1 = try store.makeVersion(basedOn: predecessors1, storing: [.update(newValue1)])\n\n        let predecessors2: Version.Predecessors = .init(idOfFirst: branch2.id, idOfSecond: nil)\n        let newValue2 = Value(id: .init(\"ABCDEF\"), data: \"Joyce\".data(using: .utf8)!)\n        branch2 = try store.makeVersion(basedOn: predecessors2, storing: [.update(newValue2)])\n\n        let mergeVersion = try store.mergeRelated(version: branch1.id, with: branch2.id, resolvingWith: Arbiter())\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(mergeValue.data == \"Joyce\".data(using: .utf8)!)\n    }\n\n    @Test func twoWayMerge() throws {\n        let secondValue = Value(id: .init(\"CDEFGH\"), data: \"Dave\".data(using: .utf8)!)\n        let newValue = Value(id: .init(\"ABCDEF\"), data: \"Joyce\".data(using: .utf8)!)\n        let changes: [Value.Change] = [.insert(secondValue), .update(newValue)]\n        let secondVersion = try store.makeVersion(basedOn: nil, storing: changes)\n        let arbiter = MostRecentChangeFavoringArbiter()\n        let mergeVersion = try store.mergeUnrelated(version: originalVersion.id, with: secondVersion.id, resolvingWith: arbiter)\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(mergeValue.data == \"Joyce\".data(using: .utf8)!)\n        let insertedValue = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n        #expect(insertedValue.data == \"Dave\".data(using: .utf8)!)\n    }\n\n    @Test func twoWayMergeDeletion() throws {\n        let changes: [Value.Change] = []\n        let arbiter = MostRecentChangeFavoringArbiter()\n        let secondVersion = try store.makeVersion(basedOn: nil, storing: changes)\n        let mergeVersion = try store.mergeUnrelated(version: originalVersion.id, with: secondVersion.id, resolvingWith: arbiter)\n        let mergeValue = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)\n        #expect(mergeValue != nil)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MostRecentBranchArbiterTests.swift",
    "content": "//\n//  MergeArbiterTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 09/03/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class MostRecentBranchMergeArbiterTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n    let origin: Version\n\n    let recentBranchArbiter: MostRecentBranchFavoringArbiter\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n        let originVal = Value(id: .init(\"CDEFGH\"), data: \"Origin\".data(using: .utf8)!)\n        origin = try store.makeVersion(basedOnPredecessor: nil, storing: [.insert(originVal)])\n        recentBranchArbiter = MostRecentBranchFavoringArbiter()\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    @Test func remove() throws {\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)\n        #expect(f == nil)\n    }\n\n    @Test func twiceRemove() throws {\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)\n        #expect(f == nil)\n    }\n\n    @Test func insert() throws {\n        do {\n            let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n            let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver1.id)\n        }\n\n        do {\n            let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n            let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver2.id)\n        }\n    }\n\n    @Test func twiceInserted() throws {\n        let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n        let val2 = value(\"ABCDEF\", stringData: \"Tom\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val2)])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n        let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(f.data == \"Tom\".data(using: .utf8))\n        #expect(f.storedVersionId == ver2.id)\n    }\n\n    @Test func updated() throws {\n        let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n        #expect(f.data == \"Bob\".data(using: .utf8))\n        #expect(f.storedVersionId == ver1.id)\n    }\n\n    @Test func twiceUpdated() throws {\n        let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n        let val2 = value(\"CDEFGH\", stringData: \"Tom\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val2)])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n        #expect(f.data == \"Tom\".data(using: .utf8))\n        #expect(f.storedVersionId == ver2.id)\n    }\n\n    @Test func removedAndUpdated() throws {\n        do {\n            let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n            let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)\n            #expect(f == nil)\n        }\n\n        do {\n            let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentBranchArbiter)\n            let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver2.id)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MostRecentChangeArbiterTests.swift",
    "content": "//\n//  MostRecentChangeFavoringArbiterTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 10/03/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class MostRecentChangeMergeArbiterTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n    let origin: Version\n\n    let recentChangeArbiter: MostRecentChangeFavoringArbiter\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n        let originVal = Value(id: .init(\"CDEFGH\"), data: \"Origin\".data(using: .utf8)!)\n        origin = try store.makeVersion(basedOnPredecessor: nil, storing: [.insert(originVal)])\n        recentChangeArbiter = MostRecentChangeFavoringArbiter()\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    @Test func remove() throws {\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)\n        #expect(f == nil)\n    }\n\n    @Test func twiceRemove() throws {\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n        let ver3 = try store.makeVersion(basedOnPredecessor: ver2.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver3.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)\n        #expect(f == nil)\n    }\n\n    @Test func insert() throws {\n        do {\n            let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n            let ver3 = try store.makeVersion(basedOnPredecessor: ver1.id, storing: [])\n            let mergeVersion = try store.mergeRelated(version: ver3.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n            let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver1.id)\n        }\n\n        do {\n            let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n            let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver2.id)\n        }\n    }\n\n    @Test func twiceInserted() throws {\n        let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n        let val2 = value(\"ABCDEF\", stringData: \"Tom\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val2)])\n        let ver3 = try store.makeVersion(basedOnPredecessor: ver1.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver3.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(f.data == \"Tom\".data(using: .utf8))\n        #expect(f.storedVersionId == ver2.id)\n    }\n\n    @Test func twiceInsertedAndUpdated() throws {\n        let val1 = value(\"ABCDEF\", stringData: \"Bob\")\n        let val2 = value(\"ABCDEF\", stringData: \"Tom\")\n        let val3 = value(\"ABCDEF\", stringData: \"Dave\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val2)])\n        let ver3 = try store.makeVersion(basedOnPredecessor: ver1.id, storing: [.update(val3)])\n        let mergeVersion = try store.mergeRelated(version: ver3.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"ABCDEF\"), at: mergeVersion.id)!\n        #expect(f.data == \"Dave\".data(using: .utf8))\n        #expect(f.storedVersionId == ver3.id)\n    }\n\n    @Test func updated() throws {\n        let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [])\n        let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n        #expect(f.data == \"Bob\".data(using: .utf8))\n        #expect(f.storedVersionId == ver1.id)\n    }\n\n    @Test func twiceUpdated() throws {\n        let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n        let val2 = value(\"CDEFGH\", stringData: \"Tom\")\n        let val3 = value(\"CDEFGH\", stringData: \"Dave\")\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val2)])\n        let ver3 = try store.makeVersion(basedOnPredecessor: ver1.id, storing: [.update(val3)])\n        let mergeVersion = try store.mergeRelated(version: ver3.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n        let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n        #expect(f.data == \"Dave\".data(using: .utf8))\n        #expect(f.storedVersionId == ver3.id)\n    }\n\n    @Test func removedAndUpdated() throws {\n        do {\n            let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n            let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver1.id)\n        }\n        do {\n            let val1 = value(\"CDEFGH\", stringData: \"Bob\")\n            let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.remove(.init(\"CDEFGH\"))])\n            let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.update(val1)])\n            let mergeVersion = try store.mergeRelated(version: ver1.id, with: ver2.id, resolvingWith: recentChangeArbiter)\n            let f = try store.value(id: .init(\"CDEFGH\"), at: mergeVersion.id)!\n            #expect(f.data == \"Bob\".data(using: .utf8))\n            #expect(f.storedVersionId == ver2.id)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/MultipeerExchangeTests.swift",
    "content": "//\n//  MultipeerExchangeTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n/// A mock transport that routes data between two MultipeerExchange instances.\nclass MockPeerTransport: PeerTransport {\n    var peers: [String: MultipeerExchange] = [:]\n\n    func send(_ data: Data, toPeer peerID: String) throws {\n        guard let exchange = peers[peerID] else {\n            throw MultipeerExchange.Error.transportUnavailable\n        }\n        // Deliver asynchronously to simulate real network\n        Task {\n            exchange.receiveData(data)\n        }\n    }\n}\n\n@Suite class MultipeerExchangeTests {\n\n    let fm = FileManager.default\n\n    let store1: Store\n    let store2: Store\n    let rootURL1: URL\n    let rootURL2: URL\n    let exchange1: MultipeerExchange\n    let exchange2: MultipeerExchange\n    let transport: MockPeerTransport\n\n    init() throws {\n        rootURL1 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL1)\n        store2 = try Store(rootDirectoryURL: rootURL2)\n\n        transport = MockPeerTransport()\n\n        // exchange1 sends to \"peer2\", exchange2 sends to \"peer1\"\n        exchange1 = MultipeerExchange(store: store1, peerID: \"peer2\", transport: transport)\n        exchange2 = MultipeerExchange(store: store2, peerID: \"peer1\", transport: transport)\n\n        // Register exchanges so transport can deliver messages\n        transport.peers[\"peer1\"] = exchange1\n        transport.peers[\"peer2\"] = exchange2\n    }\n\n    deinit {\n        try? fm.removeItem(at: rootURL1)\n        try? fm.removeItem(at: rootURL2)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    // MARK: - Tests\n\n    @Test func retrieveVersionIdentifiers() async throws {\n        let val = value(\"AABBCC\", stringData: \"Hello\")\n        _ = try store2.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let ids = try await exchange1.retrieveAllVersionIdentifiers()\n        #expect(ids.count == 1)\n    }\n\n    @Test func retrieveVersions() async throws {\n        let val = value(\"AABBCC\", stringData: \"Hello\")\n        let ver = try store2.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let versions = try await exchange1.retrieveVersions(identifiedBy: [ver.id])\n        #expect(versions.count == 1)\n        #expect(versions.first?.id == ver.id)\n    }\n\n    @Test func retrieveValueChanges() async throws {\n        let val = value(\"AABBCC\", stringData: \"Hello\")\n        let ver = try store2.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        let changesByVersion = try await exchange1.retrieveValueChanges(forVersionsIdentifiedBy: [ver.id])\n        #expect(changesByVersion.count == 1)\n        #expect(changesByVersion[ver.id]?.count == 1)\n    }\n\n    @Test func fullSendThenRetrieveCycle() async throws {\n        let val = value(\"AABBCC\", stringData: \"Hello\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        // Send from store1 to store2's exchange\n        let sentIds = try await exchange1.send()\n        #expect(sentIds.contains(ver.id))\n\n        // Retrieve into store2 from store1's exchange\n        let retrievedIds = try await exchange2.retrieve()\n        #expect(retrievedIds.contains(ver.id))\n\n        // Verify store2 now has the version\n        let storedVersion = try store2.version(identifiedBy: ver.id)\n        #expect(storedVersion != nil)\n\n        // Verify value is accessible\n        let storedVal = try store2.value(id: val.id, at: ver.id)\n        #expect(storedVal != nil)\n        #expect(storedVal?.data == val.data)\n    }\n\n    @Test func bidirectionalSync() async throws {\n        // Store1 creates a version\n        let val1 = value(\"AAA\", stringData: \"From store1\")\n        let ver1 = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val1)])\n\n        // Store2 creates a version\n        let val2 = value(\"BBB\", stringData: \"From store2\")\n        let ver2 = try store2.makeVersion(basedOnPredecessor: nil, storing: [.insert(val2)])\n\n        // Send from store1 -> store2's exchange\n        _ = try await exchange1.send()\n\n        // Retrieve into store2 from store1's exchange\n        _ = try await exchange2.retrieve()\n\n        // Send from store2 -> store1's exchange\n        _ = try await exchange2.send()\n\n        // Retrieve into store1 from store2's exchange\n        _ = try await exchange1.retrieve()\n\n        // Both stores should have both versions\n        #expect(try store1.version(identifiedBy: ver2.id) != nil)\n        #expect(try store2.version(identifiedBy: ver1.id) != nil)\n    }\n\n    @Test func pushToRecipient() async throws {\n        let val = value(\"PUSHVAL\", stringData: \"Pushed data\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        // Prepare the version changes to push\n        let changes = try store1.valueChanges(madeInVersionIdentifiedBy: ver.id)\n        let versionChanges: [VersionChanges] = [(ver, changes)]\n\n        try await exchange1.send(versionChanges: versionChanges)\n\n        // Poll for async delivery (push is fire-and-forget with two async hops)\n        var storedVersion: Version? = nil\n        for _ in 0..<100 {\n            try await Task.sleep(nanoseconds: 50_000_000) // 50ms\n            storedVersion = try store2.version(identifiedBy: ver.id)\n            if storedVersion != nil { break }\n        }\n\n        #expect(storedVersion != nil)\n    }\n\n    @Test func timeoutOnDisconnectedPeer() async throws {\n        // Create an exchange with no transport\n        let disconnectedExchange = MultipeerExchange(store: store1, peerID: \"nonexistent\", transport: nil)\n\n        do {\n            _ = try await disconnectedExchange.retrieveAllVersionIdentifiers()\n            Issue.record(\"Should have thrown transportUnavailable\")\n        } catch MultipeerExchange.Error.transportUnavailable {\n            // Expected\n        }\n    }\n\n    @Test func emptyStoreReturnsNoVersions() async throws {\n        let ids = try await exchange1.retrieveAllVersionIdentifiers()\n        #expect(ids.count == 0)\n    }\n\n    @Test func newVersionsAvailableOnPush() async throws {\n        let val = value(\"NOTIFY\", stringData: \"Notification test\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n\n        // Listen for new versions on exchange2\n        let notified = Task<Bool, Never> {\n            for await _ in exchange2.newVersionsAvailable {\n                return true\n            }\n            return false\n        }\n\n        // Give the listener time to start\n        try await Task.sleep(nanoseconds: 50_000_000)\n\n        // Push from store1 to store2\n        let changes = try store1.valueChanges(madeInVersionIdentifiedBy: ver.id)\n        try await exchange1.send(versionChanges: [(ver, changes)])\n\n        // Wait for notification\n        let result = await Task {\n            try? await Task.sleep(nanoseconds: 500_000_000)\n            notified.cancel()\n            return await notified.value\n        }.value\n\n        #expect(result)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/PerformanceTests.swift",
    "content": "//\n//  PerformanceTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 14/02/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class PerformanceTests {\n\n    let fm = FileManager.default\n\n    let valueId1 = Value.ID(\"ABCDEF\")\n    let valueId2 = Value.ID(\"ABCDGH\")\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    func makeChanges(_ number: Int) -> [Value.Change]  {\n        return (0..<number).map { _ in\n            let data = try! JSONSerialization.data(withJSONObject: [\"name\":\"Tom Jones\", \"age\":18] as [String:Any], options: [])\n            let value = Value(id: .init(UUID().uuidString), data: data)\n            return .insert(value)\n        }\n    }\n\n    let numberOfValues = 100\n\n    @Test func storing() throws {\n        let changes = makeChanges(numberOfValues)\n        let _ = try store.makeVersion(basedOnPredecessor: nil, storing: changes)\n    }\n\n    @Test func loading() throws {\n        let changes = makeChanges(numberOfValues)\n        let valueIds: [Value.ID] = changes.compactMap { change in\n            if case let .insert(value) = change { return value.id }\n            fatalError()\n        }\n        let version = try store.makeVersion(basedOnPredecessor: nil, storing: changes)\n        let _: [Any] = valueIds.map { valueId in\n            let value = try! store.value(id: valueId, at: version.id)!\n            return try! JSONSerialization.jsonObject(with: value.data, options: [])\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/PrevailingValueTests.swift",
    "content": "//\n//  PrevailingValueTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 23/11/2018.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class PrevailingValueTests {\n\n    let fm = FileManager.default\n    let valueId = Value.ID(\"ABCDEF\")\n\n    let store: Store\n    let rootURL: URL\n    let valuesURL: URL\n    let versions: [Version]\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        valuesURL = rootURL.appendingPathComponent(\"values\")\n        let s = try Store(rootDirectoryURL: rootURL)\n        store = s\n\n        var vers: [Version] = []\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: []))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.insert(Value(id: Value.ID(\"ABCDEF\"), data: \"1\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: []))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.insert(Value(id: Value.ID(\"ABCDEF\"), data: \"2\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.insert(Value(id: Value.ID(\"ABCDEF\"), data: \"3\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: []))\n        versions = vers\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func noSavedVersionAtPrevailingVersion() throws {\n        #expect(try store.value(id: valueId, at: versions[0].id) == nil)\n    }\n\n    @Test func savedVersionMatchesPrevailingVersion() throws {\n        let value = try store.value(id: valueId, at: versions[1].id)\n        #expect(value!.data == \"1\".data(using: .utf8)!)\n    }\n\n    @Test func savedVersionPrecedesPrevailingVersion() throws {\n        let value = try store.value(id: valueId, at: versions[5].id)\n        #expect(value!.data == \"3\".data(using: .utf8)!)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/SQLitePerformanceTests.swift",
    "content": "//\n//  SQLitePerformanceTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 16/02/2022.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n@testable import LLVSSQLite\n\n@Suite class SQLitePerformanceTests {\n\n    let fm = FileManager.default\n\n    let valueId1 = Value.ID(\"ABCDEF\")\n    let valueId2 = Value.ID(\"ABCDGH\")\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        let storage = SQLiteStorage()\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL, storage: storage)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    func makeChanges(_ number: Int) -> [Value.Change]  {\n        return (0..<number).map { _ in\n            let data = try! JSONSerialization.data(withJSONObject: [\"name\":\"Tom Jones\", \"age\":18] as [String:Any], options: [])\n            let value = Value(id: .init(UUID().uuidString), data: data)\n            return .insert(value)\n        }\n    }\n\n    let numberOfValues = 100\n\n    @Test func storing() throws {\n        let changes = makeChanges(numberOfValues)\n        let _ = try store.makeVersion(basedOnPredecessor: nil, storing: changes)\n    }\n\n    @Test func loading() throws {\n        let changes = makeChanges(numberOfValues)\n        let valueIds: [Value.ID] = changes.compactMap { change in\n            if case let .insert(value) = change { return value.id }\n            fatalError()\n        }\n        let version = try store.makeVersion(basedOnPredecessor: nil, storing: changes)\n        let _: [Any] = valueIds.map { valueId in\n            let value = try! store.value(id: valueId, at: version.id)!\n            return try! JSONSerialization.jsonObject(with: value.data, options: [])\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/SQLiteZoneTests.swift",
    "content": "//\n//  SQLiteZoneTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 16/02/2022.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n@testable import LLVSSQLite\n\n@Suite class SQLiteZoneTests {\n\n    let fm = FileManager.default\n\n    let zone: SQLiteZone\n    let rootURL: URL\n    let ref: ZoneReference\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        zone = try SQLiteZone(rootDirectory: rootURL, fileExtension: \"sqlite\")\n        ref = ZoneReference(key: \"ABCDEF\", version: .init(\"1234\"))\n    }\n\n    deinit {\n        try? zone.dismantle()\n        try? fm.removeItem(at: rootURL)\n    }\n\n    @Test func creation() {\n        #expect(fm.fileExists(atPath: rootURL.path))\n        #expect(fm.fileExists(atPath: rootURL.appendingPathComponent(\"zone.sqlite\").path))\n    }\n\n    @Test func addingMultipleReferencesWithSameKey() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: ref.key, version: .init(\"1245\")))\n    }\n\n    @Test func addingMultipleReferencesWithSameVersion() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: \"ABCDEFG\", version: ref.version))\n    }\n\n    @Test func retrievingNonExistentData() throws {\n        let data = try zone.data(for: ref)\n        #expect(data == nil)\n    }\n\n    @Test func retrievingData() throws {\n        try zone.store(\"Test\".data(using: .utf8)!, for: ref)\n        let data = try zone.data(for: ref)\n        #expect(data != nil)\n        let string = String(bytes: data!, encoding: .utf8)\n        #expect(string == \"Test\")\n    }\n\n    @Test func versionsQuery() throws {\n        try zone.store(Data(), for: ref)\n        try zone.store(Data(), for: .init(key: \"ABCDEF\", version: .init(\"1245\")))\n        let versions = try zone.versionIds(for: \"ABCDEF\")\n        let versionStrings = versions.map { $0.rawValue }\n        #expect(versions.count == 2)\n        #expect(versionStrings.contains(\"1234\"))\n        #expect(versionStrings.contains(\"1245\"))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/SerialHistoryTests.swift",
    "content": "//\n//  SerialHistoryTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 04/02/2019.\n//\n\nimport Testing\nimport Foundation\n\n@testable import LLVS\n\n@Suite class SerialHistoryTests {\n\n    let fm = FileManager.default\n\n    let valueId1 = Value.ID(\"ABCDEF\")\n    let valueId2 = Value.ID(\"ABCDGH\")\n\n    let store: Store\n    let rootURL: URL\n    let versions: [Version]\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let s = try Store(rootDirectoryURL: rootURL)\n        store = s\n\n        var vers: [Version] = []\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDEF\"), data: \"11\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDGH\"), data: \"21\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDEF\"), data: \"12\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDGH\"), data: \"22\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDEF\"), data: \"13\".data(using: .utf8)!))]))\n        vers.append(try s.makeVersion(basedOnPredecessor: vers.last?.id, storing: [.update(Value(id: Value.ID(\"ABCDGH\"), data: \"23\".data(using: .utf8)!))]))\n        versions = vers\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func valuesThroughoutHistory() throws {\n        #expect(try store.value(id: valueId1, at: versions[1].id)!.data == \"11\".data(using: .utf8))\n        #expect(try store.value(id: valueId2, at: versions[1].id)!.data == \"21\".data(using: .utf8))\n        #expect(try store.value(id: valueId1, at: versions[2].id)!.data == \"12\".data(using: .utf8))\n        #expect(try store.value(id: valueId2, at: versions[2].id)!.data == \"21\".data(using: .utf8))\n        #expect(try store.value(id: valueId1, at: versions[3].id)!.data == \"12\".data(using: .utf8))\n        #expect(try store.value(id: valueId2, at: versions[3].id)!.data == \"22\".data(using: .utf8))\n        #expect(try store.value(id: valueId1, at: versions[4].id)!.data == \"13\".data(using: .utf8))\n        #expect(try store.value(id: valueId2, at: versions[4].id)!.data == \"22\".data(using: .utf8))\n        #expect(try store.value(id: valueId1, at: versions[5].id)!.data == \"13\".data(using: .utf8))\n        #expect(try store.value(id: valueId2, at: versions[5].id)!.data == \"23\".data(using: .utf8))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/SharedStoreTests.swift",
    "content": "//\n//  SharedStoreTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 16/03/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class SharedStoreTests {\n\n    let fm = FileManager.default\n\n    let store1: Store\n    let store2: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL)\n        store2 = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    private func value(_ identifier: String, stringData: String) -> Value {\n        return Value(id: .init(identifier), data: stringData.data(using: .utf8)!)\n    }\n\n    @Test func reloadHistoryInSecondStore() throws {\n        let val = value(\"CDEFGH\", stringData: \"Origin\")\n        let ver = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val)])\n        try store2.reloadHistory()\n        let version = try store2.version(identifiedBy: ver.id)\n        #expect(version != nil)\n        let retrievedData = try store2.value(id: .init(\"CDEFGH\"), storedAt: ver.id)!.data\n        #expect(val.data == retrievedData)\n    }\n\n    @Test func twoWayTransfer() throws {\n        let val1 = value(\"CDEFGH\", stringData: \"One\")\n        let ver1 = try store1.makeVersion(basedOnPredecessor: nil, storing: [.insert(val1)])\n\n        try store2.reloadHistory()\n        let val2 = value(\"CDEFGH\", stringData: \"Two\")\n        let ver2 = try store1.makeVersion(basedOnPredecessor: ver1.id, storing: [.update(val2)])\n\n        try store1.reloadHistory()\n        let version = try store1.version(identifiedBy: ver2.id)\n        #expect(version != nil)\n        let retrievedData = try store1.value(id: .init(\"CDEFGH\"), storedAt: ver2.id)!.data\n        #expect(val2.data == retrievedData)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/SnapshotTests.swift",
    "content": "//\n//  SnapshotTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 09/02/2026.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n@testable import LLVSSQLite\n\n// MARK: - Storage-Level Tests\n\n@Suite class SnapshotStorageTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    private func value(_ id: String, _ string: String) -> Value {\n        Value(id: .init(id), data: string.data(using: .utf8)!)\n    }\n\n    @discardableResult\n    private func makeLinearChain(count: Int, store: Store? = nil) -> [Version] {\n        let s = store ?? self.store\n        var versions: [Version] = []\n        var predecessor: Version.ID? = nil\n        for i in 0..<count {\n            let val = value(\"val\\(i)\", \"data\\(i)\")\n            let ver = try! s.makeVersion(basedOnPredecessor: predecessor, storing: [.insert(val)])\n            versions.append(ver)\n            predecessor = ver.id\n        }\n        return versions\n    }\n\n    @Test func fileStorageSnapshotRoundTrip() throws {\n        let versions = makeLinearChain(count: 50)\n        let storage = FileStorage()\n\n        // Write snapshot\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        // Create a new empty store and restore\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        try fm.createDirectory(at: rootURL2, withIntermediateDirectories: true, attributes: nil)\n\n        try storage.restoreFromSnapshotChunks(storeRootURL: rootURL2, from: snapshotDir, manifest: manifest)\n\n        // Load the store and verify\n        let store2 = try Store(rootDirectoryURL: rootURL2)\n        var versionCount = 0\n        store2.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        #expect(versionCount == 50)\n\n        // Verify all values readable\n        for version in versions {\n            let refs = try store2.valueReferences(at: version.id)\n            #expect(!refs.isEmpty)\n        }\n\n        // Verify latest value readable\n        let latestVal = try store2.value(id: .init(\"val49\"), at: versions.last!.id)\n        #expect(latestVal != nil)\n        #expect(String(data: latestVal!.data, encoding: .utf8) == \"data49\")\n    }\n\n    @Test func snapshotChunking() throws {\n        // Create enough data to produce multiple chunks\n        let versions = makeLinearChain(count: 50)\n        let storage = FileStorage()\n\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n\n        // Use a very small chunk size to ensure multiple chunks\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL, to: snapshotDir, maxChunkSize: 1024)\n\n        #expect(manifest.chunkCount > 1, \"Small maxChunkSize should produce multiple chunks\")\n\n        // Verify round-trip with chunked snapshot\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        try fm.createDirectory(at: rootURL2, withIntermediateDirectories: true, attributes: nil)\n\n        try storage.restoreFromSnapshotChunks(storeRootURL: rootURL2, from: snapshotDir, manifest: manifest)\n        let store2 = try Store(rootDirectoryURL: rootURL2)\n\n        // Verify integrity\n        let latestVal = try store2.value(id: .init(\"val49\"), at: versions.last!.id)\n        #expect(latestVal != nil)\n        #expect(String(data: latestVal!.data, encoding: .utf8) == \"data49\")\n    }\n\n    @Test func snapshotManifestContents() throws {\n        makeLinearChain(count: 50)\n        let storage = FileStorage()\n\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        #expect(manifest.format == \"zip-v1\")\n        #expect(manifest.versionCount == 50)\n        #expect(manifest.chunkCount > 0)\n        #expect(!manifest.latestVersionId.rawValue.isEmpty)\n        #expect(manifest.totalSize > 0)\n    }\n\n    @Test func sqliteStorageSnapshotRoundTrip() throws {\n        // Create store with SQLite storage\n        try? fm.removeItem(at: rootURL)\n        let sqlStore = try Store(rootDirectoryURL: rootURL, storage: SQLiteStorage())\n        let versions = makeLinearChain(count: 50, store: sqlStore)\n        let storage = SQLiteStorage()\n\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL, to: snapshotDir, maxChunkSize: 5_000_000)\n        #expect(manifest.format == \"zip-v1\")\n        #expect(manifest.versionCount == 50)\n\n        // Restore\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        try fm.createDirectory(at: rootURL2, withIntermediateDirectories: true, attributes: nil)\n\n        try storage.restoreFromSnapshotChunks(storeRootURL: rootURL2, from: snapshotDir, manifest: manifest)\n        let store2 = try Store(rootDirectoryURL: rootURL2, storage: SQLiteStorage())\n\n        var versionCount = 0\n        store2.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        #expect(versionCount == 50)\n\n        let latestVal = try store2.value(id: .init(\"val49\"), at: versions.last!.id)\n        #expect(latestVal != nil)\n        #expect(String(data: latestVal!.data, encoding: .utf8) == \"data49\")\n    }\n}\n\n\n// MARK: - Exchange-Level Tests\n\n@Suite class SnapshotExchangeTests {\n\n    let fm = FileManager.default\n\n    let store1: Store\n    let rootURL1: URL\n    let exchangeURL: URL\n    let exchange1: FileSystemExchange\n\n    init() throws {\n        rootURL1 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store1 = try Store(rootDirectoryURL: rootURL1)\n        exchangeURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        exchange1 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store1, usesFileCoordination: false)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL1)\n        try? FileManager.default.removeItem(at: exchangeURL)\n    }\n\n    private func value(_ id: String, _ string: String) -> Value {\n        Value(id: .init(id), data: string.data(using: .utf8)!)\n    }\n\n    @discardableResult\n    private func makeLinearChain(count: Int, store: Store? = nil) -> [Version] {\n        let s = store ?? self.store1\n        var versions: [Version] = []\n        var predecessor: Version.ID? = nil\n        for i in 0..<count {\n            let val = value(\"val\\(i)\", \"data\\(i)\")\n            let ver = try! s.makeVersion(basedOnPredecessor: predecessor, storing: [.insert(val)])\n            versions.append(ver)\n            predecessor = ver.id\n        }\n        return versions\n    }\n\n    @Test func fileSystemExchangeSnapshotUploadDownload() async throws {\n        makeLinearChain(count: 50)\n        let storage = FileStorage()\n\n        // Write snapshot\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        // Upload via exchange\n        try await exchange1.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Verify files exist in snapshots/ directory\n        let snapshotsDir = exchangeURL.appendingPathComponent(\"snapshots\")\n        #expect(fm.fileExists(atPath: snapshotsDir.appendingPathComponent(\"manifest.json\").path))\n\n        // Download manifest from second exchange instance\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        let store2 = try Store(rootDirectoryURL: rootURL2)\n        let exchange2 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: false)\n\n        let downloadedManifest = try await exchange2.retrieveSnapshotManifest()\n        #expect(downloadedManifest != nil)\n        #expect(downloadedManifest?.format == \"zip-v1\")\n        #expect(downloadedManifest?.versionCount == 50)\n    }\n\n    @Test func bootstrapFromSnapshot() async throws {\n        let versions = makeLinearChain(count: 50)\n\n        // Sync to exchange\n        let _ = try await exchange1.send()\n\n        // Upload snapshot\n        let storage = FileStorage()\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        try await exchange1.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Create store2 coordinator and bootstrap\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL2)\n            try? fm.removeItem(at: cacheURL2)\n        }\n\n        let coordinator2 = try StoreCoordinator(withStoreDirectoryAt: rootURL2, cacheDirectoryAt: cacheURL2)\n        coordinator2.exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator2.store, usesFileCoordination: false)\n\n        try await coordinator2.bootstrapFromSnapshot()\n\n        // Verify all versions: 50 from snapshot + 1 initial from coordinator2\n        var versionCount = 0\n        coordinator2.store.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        #expect(versionCount == 51)\n\n        // Verify data readable\n        let latestVal = try coordinator2.store.value(id: .init(\"val49\"), at: versions.last!.id)\n        #expect(latestVal != nil)\n    }\n\n    @Test func bootstrapThenIncrementalSync() async throws {\n        let versions = makeLinearChain(count: 50)\n\n        // Upload snapshot\n        let storage = FileStorage()\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        try await exchange1.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Add 10 more versions and sync to exchange\n        var predecessor = versions.last!.id\n        for i in 50..<60 {\n            let ver = try store1.makeVersion(basedOnPredecessor: predecessor, storing: [.insert(value(\"val\\(i)\", \"data\\(i)\"))])\n            predecessor = ver.id\n        }\n\n        let _ = try await exchange1.send()\n\n        // Create store2, bootstrap, then incremental sync\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL2)\n            try? fm.removeItem(at: cacheURL2)\n        }\n\n        let coordinator2 = try StoreCoordinator(withStoreDirectoryAt: rootURL2, cacheDirectoryAt: cacheURL2)\n        let exchange2 = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator2.store, usesFileCoordination: false)\n        coordinator2.exchange = exchange2\n\n        try await coordinator2.bootstrapFromSnapshot()\n\n        // Normal exchange to get remaining 10 versions\n        try await coordinator2.exchange()\n\n        // Should have all versions: 60 from store1 + 1 initial from coordinator2\n        var versionCount = 0\n        coordinator2.store.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        #expect(versionCount == 61)\n\n        let val59 = try coordinator2.store.value(id: .init(\"val59\"), at: .init(predecessor.rawValue))\n        #expect(val59 != nil)\n    }\n\n    @Test func concurrentSnapshotUploadsProduceValidSnapshot() async throws {\n        // Two stores upload snapshots concurrently to the same exchange directory.\n        // After both complete, the resulting snapshot should be valid and bootstrappable.\n        makeLinearChain(count: 30)\n\n        // Create a second store with different data\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        let store2 = try Store(rootDirectoryURL: rootURL2)\n        var predecessor: Version.ID? = nil\n        for i in 0..<40 {\n            let val = value(\"other\\(i)\", \"otherdata\\(i)\")\n            let ver = try store2.makeVersion(basedOnPredecessor: predecessor, storing: [.insert(val)])\n            predecessor = ver.id\n        }\n\n        let storage = FileStorage()\n\n        // Build snapshot chunks from each store\n        let snapshotDirA = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let snapshotDirB = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: snapshotDirA)\n            try? fm.removeItem(at: snapshotDirB)\n        }\n\n        let manifestA = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDirA, maxChunkSize: 1024)\n        let manifestB = try storage.writeSnapshotChunks(storeRootURL: rootURL2, to: snapshotDirB, maxChunkSize: 1024)\n\n        // Two separate exchange instances pointing to the same shared directory\n        let exchangeA = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store1, usesFileCoordination: false)\n        let exchangeB = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: false)\n\n        // Upload concurrently\n        await withTaskGroup(of: Void.self) { group in\n            group.addTask {\n                try? await exchangeA.sendSnapshot(manifest: manifestA, chunkProvider: { index in\n                    let chunkFile = snapshotDirA.appendingPathComponent(String(format: \"chunk-%03d\", index))\n                    return try Data(contentsOf: chunkFile)\n                })\n            }\n            group.addTask {\n                try? await exchangeB.sendSnapshot(manifest: manifestB, chunkProvider: { index in\n                    let chunkFile = snapshotDirB.appendingPathComponent(String(format: \"chunk-%03d\", index))\n                    return try Data(contentsOf: chunkFile)\n                })\n            }\n        }\n\n        // Now retry upload from store2 to guarantee a clean snapshot exists\n        try await exchangeB.sendSnapshot(manifest: manifestB, chunkProvider: { index in\n            let chunkFile = snapshotDirB.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Verify the final snapshot is valid by bootstrapping a third store\n        let rootURL3 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL3 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL3)\n            try? fm.removeItem(at: cacheURL3)\n        }\n\n        // Send store2's versions so the bootstrapped store can load them\n        let exchange2ForSend = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: false)\n        let _ = try await exchange2ForSend.send()\n\n        let coordinator3 = try StoreCoordinator(withStoreDirectoryAt: rootURL3, cacheDirectoryAt: cacheURL3)\n        coordinator3.exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator3.store, usesFileCoordination: false)\n\n        try await coordinator3.bootstrapFromSnapshot()\n\n        var versionCount = 0\n        coordinator3.store.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        // 40 versions from store2's snapshot + 1 initial from coordinator3\n        #expect(versionCount == 41)\n    }\n\n    @Test func bootstrapDuringSnapshotReplacementRecoversGracefully() async throws {\n        // Upload an initial snapshot, then start a bootstrap while simultaneously\n        // replacing the snapshot. The bootstrap should either succeed or fail with\n        // an error (not crash or corrupt the store).\n        makeLinearChain(count: 30)\n\n        let storage = FileStorage()\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDir, maxChunkSize: 1024)\n\n        // Upload initial snapshot\n        try await exchange1.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Send versions to exchange so bootstrap can work\n        let _ = try await exchange1.send()\n\n        // Prepare replacement snapshot from a different store\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: rootURL2) }\n        let store2 = try Store(rootDirectoryURL: rootURL2)\n        var predecessor: Version.ID? = nil\n        for i in 0..<20 {\n            let ver = try store2.makeVersion(basedOnPredecessor: predecessor, storing: [.insert(value(\"new\\(i)\", \"newdata\\(i)\"))])\n            predecessor = ver.id\n        }\n        let snapshotDir2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir2) }\n        let manifest2 = try storage.writeSnapshotChunks(storeRootURL: rootURL2, to: snapshotDir2, maxChunkSize: 1024)\n\n        // Now race: bootstrap from one exchange while another replaces the snapshot\n        let rootURL3 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL3 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL3)\n            try? fm.removeItem(at: cacheURL3)\n        }\n\n        let coordinator3 = try StoreCoordinator(withStoreDirectoryAt: rootURL3, cacheDirectoryAt: cacheURL3)\n        coordinator3.exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator3.store, usesFileCoordination: false)\n\n        let replacerExchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: store2, usesFileCoordination: false)\n\n        var bootstrapError: Swift.Error?\n\n        // Race bootstrap and snapshot replacement\n        await withTaskGroup(of: Void.self) { group in\n            group.addTask {\n                do {\n                    try await coordinator3.bootstrapFromSnapshot()\n                } catch {\n                    bootstrapError = error\n                }\n            }\n            group.addTask {\n                try? await replacerExchange.sendSnapshot(manifest: manifest2, chunkProvider: { index in\n                    let chunkFile = snapshotDir2.appendingPathComponent(String(format: \"chunk-%03d\", index))\n                    return try Data(contentsOf: chunkFile)\n                })\n            }\n        }\n\n        // The key assertion: no crash, and the store is in a consistent state.\n        if bootstrapError != nil {\n            // Bootstrap failed due to race — this is the expected graceful recovery.\n            var versionCount = 0\n            coordinator3.store.queryHistory { history in\n                versionCount = history.allVersionIdentifiers.count\n            }\n            #expect(versionCount >= 1)\n        }\n\n        // Regardless of the race outcome, a fresh bootstrap with the current snapshot should work.\n        let rootURL4 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL4 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL4)\n            try? fm.removeItem(at: cacheURL4)\n        }\n\n        let coordinator4 = try StoreCoordinator(withStoreDirectoryAt: rootURL4, cacheDirectoryAt: cacheURL4)\n        coordinator4.exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator4.store, usesFileCoordination: false)\n\n        try await coordinator4.bootstrapFromSnapshot()\n\n        // The second snapshot (from store2) should now be in effect\n        var retryCount = 0\n        coordinator4.store.queryHistory { history in\n            retryCount = history.allVersionIdentifiers.count\n        }\n        // 20 versions from store2 snapshot + 1 initial from coordinator4\n        #expect(retryCount == 21)\n    }\n\n    @Test func bootstrapSkipsPopulatedStore() async throws {\n        makeLinearChain(count: 50)\n\n        // Upload snapshot\n        let storage = FileStorage()\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL1, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        try await exchange1.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        // Create coordinator2 with existing data (> 1 version)\n        let rootURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        let cacheURL2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer {\n            try? fm.removeItem(at: rootURL2)\n            try? fm.removeItem(at: cacheURL2)\n        }\n\n        let coordinator2 = try StoreCoordinator(withStoreDirectoryAt: rootURL2, cacheDirectoryAt: cacheURL2)\n        // Make some versions so the store is populated\n        try coordinator2.save(inserting: [value(\"existing1\", \"data1\")])\n        try coordinator2.save(inserting: [value(\"existing2\", \"data2\")])\n\n        coordinator2.exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator2.store, usesFileCoordination: false)\n\n        var versionCountBefore = 0\n        coordinator2.store.queryHistory { history in\n            versionCountBefore = history.allVersionIdentifiers.count\n        }\n\n        try await coordinator2.bootstrapFromSnapshot()\n\n        // Version count should be unchanged (bootstrap was skipped)\n        var versionCountAfter = 0\n        coordinator2.store.queryHistory { history in\n            versionCountAfter = history.allVersionIdentifiers.count\n        }\n        #expect(versionCountBefore == versionCountAfter)\n    }\n}\n\n\n// MARK: - Policy Tests\n\n@Suite class SnapshotPolicyTests {\n\n    let fm = FileManager.default\n\n    let rootURL: URL\n    let cacheURL: URL\n    let exchangeURL: URL\n\n    init() {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        cacheURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        exchangeURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n        try? FileManager.default.removeItem(at: cacheURL)\n        try? FileManager.default.removeItem(at: exchangeURL)\n    }\n\n    private func value(_ id: String, _ string: String) -> Value {\n        Value(id: .init(id), data: string.data(using: .utf8)!)\n    }\n\n    @Test func snapshotNotUploadedWhenDisabled() async throws {\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL, snapshotPolicy: .disabled)\n        let exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator.store, usesFileCoordination: false)\n        coordinator.exchange = exchange\n\n        // Create some versions\n        for i in 0..<10 {\n            try coordinator.save(inserting: [value(\"val\\(i)\", \"data\\(i)\")])\n        }\n\n        // Exchange\n        try await coordinator.exchange()\n\n        // Give the fire-and-forget snapshot upload time to complete (if it were to run)\n        try await Task.sleep(for: .seconds(2))\n\n        // Verify no snapshot directory\n        let snapshotsDir = exchangeURL.appendingPathComponent(\"snapshots\")\n        #expect(!fm.fileExists(atPath: snapshotsDir.appendingPathComponent(\"manifest.json\").path))\n    }\n\n    @Test func snapshotUploadedWhenPolicyMet() async throws {\n        let policy = SnapshotPolicy(enabled: true, minimumInterval: 0, minimumNewVersions: 5)\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL, snapshotPolicy: policy)\n        let exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator.store, usesFileCoordination: false)\n        coordinator.exchange = exchange\n\n        // Create > 5 versions\n        for i in 0..<10 {\n            try coordinator.save(inserting: [value(\"val\\(i)\", \"data\\(i)\")])\n        }\n\n        // Sync to exchange\n        try await coordinator.exchange()\n\n        // Give the async snapshot upload time to complete\n        try await Task.sleep(for: .seconds(2))\n\n        // Verify snapshot was uploaded\n        let snapshotsDir = exchangeURL.appendingPathComponent(\"snapshots\")\n        #expect(fm.fileExists(atPath: snapshotsDir.appendingPathComponent(\"manifest.json\").path), \"Snapshot should be uploaded when policy is met\")\n    }\n\n    @Test func snapshotNotReuploadedWhenRecentExists() async throws {\n        let policy = SnapshotPolicy(enabled: true, minimumInterval: 3600, minimumNewVersions: 1)\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL, snapshotPolicy: policy)\n        let exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator.store, usesFileCoordination: false)\n        coordinator.exchange = exchange\n\n        // Create versions\n        for i in 0..<10 {\n            try coordinator.save(inserting: [value(\"val\\(i)\", \"data\\(i)\")])\n        }\n\n        // Manually upload a snapshot with current timestamp\n        let storage = FileStorage()\n        let snapshotDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        defer { try? fm.removeItem(at: snapshotDir) }\n        let manifest = try storage.writeSnapshotChunks(storeRootURL: rootURL, to: snapshotDir, maxChunkSize: 5_000_000)\n\n        try await exchange.sendSnapshot(manifest: manifest, chunkProvider: { index in\n            let chunkFile = snapshotDir.appendingPathComponent(String(format: \"chunk-%03d\", index))\n            return try Data(contentsOf: chunkFile)\n        })\n\n        let originalSnapshotId = manifest.snapshotId\n\n        // Exchange again — snapshot is recent, should not be re-uploaded\n        try await coordinator.exchange()\n\n        // Wait for potential async upload\n        try await Task.sleep(for: .seconds(2))\n\n        // Verify manifest is unchanged (same snapshotId)\n        let downloaded = try await exchange.retrieveSnapshotManifest()\n        #expect(downloaded?.snapshotId == originalSnapshotId, \"Snapshot should not have been re-uploaded\")\n    }\n\n    @Test func formatCompatibilityCheck() async throws {\n        // Upload a snapshot with a mismatched format string\n        let coordinator = try StoreCoordinator(withStoreDirectoryAt: rootURL, cacheDirectoryAt: cacheURL)\n        let exchange = FileSystemExchange(rootDirectoryURL: exchangeURL, store: coordinator.store, usesFileCoordination: false)\n        coordinator.exchange = exchange\n\n        // Write a fake manifest with wrong format\n        let snapshotsDir = exchangeURL.appendingPathComponent(\"snapshots\")\n        try fm.createDirectory(at: snapshotsDir, withIntermediateDirectories: true, attributes: nil)\n        let fakeManifest = SnapshotManifest(\n            format: \"unknownFormat-v99\",\n            latestVersionId: .init(\"fake\"),\n            versionCount: 100,\n            chunkCount: 1,\n            totalSize: 1000\n        )\n        let data = try JSONEncoder().encode(fakeManifest)\n        try data.write(to: snapshotsDir.appendingPathComponent(\"manifest.json\"))\n\n        // Bootstrap should gracefully skip (no error, no restore)\n        try await coordinator.bootstrapFromSnapshot()\n\n        // Store should still have just 1 version (the initial empty one created by StoreCoordinator)\n        var versionCount = 0\n        coordinator.store.queryHistory { history in\n            versionCount = history.allVersionIdentifiers.count\n        }\n        #expect(versionCount == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/StoreSetupTests.swift",
    "content": "import Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class StoreSetupTests {\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func storeCreatesDirectories() {\n        let fm = FileManager.default\n        let root = rootURL.path as NSString\n        #expect(fm.fileExists(atPath: root as String))\n        #expect(fm.fileExists(atPath: root.appendingPathComponent(\"versions\")))\n        #expect(fm.fileExists(atPath: root.appendingPathComponent(\"values\")))\n        #expect(fm.fileExists(atPath: root.appendingPathComponent(\"maps\")))\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/ValueChangesInVersionTests.swift",
    "content": "//\n//  ValueChangesInVersionTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 13/03/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class ValueChangesInVersionTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        store = try Store(rootDirectoryURL: rootURL)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func valuesConflictlessMerge() throws {\n        let val1 = Value(id: .init(\"ABCDEF\"), data: \"Bob\".data(using: .utf8)!)\n        var val2 = Value(id: .init(\"ABCD\"), data: \"Tom\".data(using: .utf8)!)\n        let origin = try store.makeVersion(basedOn: nil, storing: [])\n        let ver1 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val1)])\n        let ver2 = try store.makeVersion(basedOnPredecessor: origin.id, storing: [.insert(val2)])\n        val2.storedVersionId = ver2.id\n        let ver3 = try store.makeVersion(basedOn: .init(idOfFirst: ver1.id, idOfSecond: ver2.id), storing: [.preserve(val2.reference!)])\n        let valueChanges = try store.valueChanges(madeInVersionIdentifiedBy: ver3.id)\n        let allPreserves = valueChanges.allSatisfy {\n            if case .preserve = $0 {\n                return true\n            } else {\n                return false\n            }\n        }\n        #expect(allPreserves)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/ValueTests.swift",
    "content": "import Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class ValueTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n    let valuesURL: URL\n    let version: Version\n    let originalValue: Value\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        valuesURL = rootURL.appendingPathComponent(\"values\")\n        store = try Store(rootDirectoryURL: rootURL)\n\n        originalValue = Value(id: .init(\"ABCDEF\"), data: \"Bob\".data(using: .utf8)!)\n        let changes: [Value.Change] = [.insert(originalValue)]\n        version = try store.makeVersion(basedOn: nil, storing: changes)\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func savingValueCreatesSubDirectoriesAndFile() {\n        let v = version.id.rawValue\n        let map = v.index(v.startIndex, offsetBy: 1)\n        let versionSubDir = String(v[..<map])\n        let versionFile = String(v[map...])\n        #expect(fm.fileExists(atPath: valuesURL.appendingPathComponent(\"AB\").path))\n        #expect(fm.fileExists(atPath: valuesURL.appendingPathComponent(\"AB/CDEF\").path))\n        #expect(fm.fileExists(atPath: valuesURL.appendingPathComponent(\"AB/CDEF/\\(versionSubDir)\").path))\n        #expect(fm.fileExists(atPath: valuesURL.appendingPathComponent(\"AB/CDEF/\\(versionSubDir)/\\(versionFile).json\").path))\n    }\n\n    @Test func savedFileContainsValue() throws {\n        let v = version.id.rawValue\n        let map = v.index(v.startIndex, offsetBy: 1)\n        let versionSubDir = String(v[..<map])\n        let versionFile = String(v[map...])\n        let file = valuesURL.appendingPathComponent(\"AB/CDEF/\\(versionSubDir)/\\(versionFile).json\")\n        let data = try Data(contentsOf: file)\n        #expect(data == \"Bob\".data(using: .utf8)!)\n    }\n\n    @Test func fetchingNonExistentVersionOfValueGivesNil() throws {\n        let version = Version(id: .init(UUID().uuidString), predecessors: nil, valueDataSize: 0)\n        let fetchedValue = try store.value(id: originalValue.id, storedAt: version.id)\n        #expect(fetchedValue == nil)\n    }\n\n    @Test func fetchingSavedVersionOfValue() throws {\n        let value = try store.value(id: originalValue.id, storedAt: version.id)\n        #expect(value != nil)\n        #expect(value!.id.rawValue == originalValue.id.rawValue)\n        #expect(value!.storedVersionId! == version.id)\n        #expect(value!.data == \"Bob\".data(using: .utf8)!)\n    }\n\n    @Test func allVersionsOfValue() throws {\n        let newValue = Value(id: .init(\"ABCDEF\"), data: \"Dave\".data(using: .utf8)!)\n        let changes: [Value.Change] = [.insert(newValue)]\n        let newVersion = try store.makeVersion(basedOn: nil, storing: changes)\n\n        let versionIds = try store.versionIds(for: newValue.id)\n\n        #expect(versionIds.count == 2)\n\n        let versions: Set<Version.ID> = [version.id, newVersion.id]\n        let fetchedVersions = Set(versionIds)\n        #expect(versions == fetchedVersions)\n    }\n}\n"
  },
  {
    "path": "Tests/LLVSTests/VersionTests.swift",
    "content": "//\n//  VersionTests.swift\n//  LLVSTests\n//\n//  Created by Drew McCormack on 26/01/2019.\n//\n\nimport Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class VersionTests {\n\n    let fm = FileManager.default\n\n    let store: Store\n    let rootURL: URL\n    let versionsURL: URL\n    let version: Version\n\n    init() throws {\n        rootURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)\n        versionsURL = rootURL.appendingPathComponent(\"versions\")\n        store = try Store(rootDirectoryURL: rootURL)\n        version = try store.makeVersion(basedOn: nil, storing: [])\n    }\n\n    deinit {\n        try? FileManager.default.removeItem(at: rootURL)\n    }\n\n    @Test func creationOfVersionFile() {\n        let v = version.id.rawValue + \".json\"\n        let prefix = String(v.prefix(2))\n        let postfix = String(v.dropFirst(2))\n        #expect(fm.fileExists(atPath: versionsURL.appendingPathComponent(prefix).appendingPathComponent(postfix).path))\n    }\n\n    @Test func loadingOfVersion() throws {\n        let store = try Store(rootDirectoryURL: rootURL)\n        store.queryHistory { history in\n            #expect(history.headIdentifiers == [version.id])\n        }\n    }\n}\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "description: >-\n  Discussion of the Low-Level Versioned Store (LLVS) and\n  other aspects of distributed data storage.\nbaseurl: \"/LLVS\"\nurl: \"https://mentalfaculty.github.io\"\n\ntheme: jekyll-theme-minimal\nplugins:\n  - jekyll-sitemap\n  - jekyll-feed\n\ntitle: LLVS Blog\ntwitter_username: drewmccormack\ngithub_username:  mentalfaculty\n"
  },
  {
    "path": "docs/_posts/2019-09-25-data-driven-swiftui.md",
    "content": "---\nlayout: post\ntitle: \"Data Driven SwiftUI\"\nauthor: Drew McCormack\ntags: [swiftui]\n---\n\nSwiftUI has set us all thinking about the future of development on Apple's platforms. It's a disruptive technology which will supersede a UI stack dating back more than 20 years to the pre-Mac OS X era. But while SwiftUI introduces bold new concepts in the UI, what about the rest of our Swift app? Can we disrupt that too? \n\nHere I'm going to show you how to build an app that...\n\n1. Uses SwiftUI for views\n2. Adopts immutable value types (structs) at every level, and immutable files on disk\n3. Syncs seamlessly across devices, with no networking code\n4. Has around 450 lines of code in total\n\n## SwiftUI\n\nEven with SwiftUI in constant flux, there is already plenty of great content around for learning the new framework. I've spent several weeks working on side projects in an effort to 'kick the tires'; the tutorials and API descriptions others have compiled have been indispensable. \n\nAnd yet, I can't help feeling most of the code is clothed above the waist, and pantsless below. The king is only half dressed. There is an enormous, data-sized elephant in the SwiftUI room.\n\nTo be more concrete, SwiftUI examples typically rely heavily on the `@State` property wrapper. This is a convenient way to include some mutable state without having to think much about the controllers and data models which make up a real app. This is fully understandable, because the framework is new, and we are still fixated on how to handle animation, layout, and a multitude of other UI concerns. But recently, I've started to focus on the next step: How do you go from the tutorials and demos to real world, scalable apps? How do you build a SwiftUI app from the ground up?\n\n## Structs Atop Classes\n\nOne of the most perplexing aspects of the current state of affairs is that we have a framework for UI based on value types — `View` structs — while being encouraged to use reference types at the model level. Model types are still typically represented by classes, and Apple's own solution for data storage, Core Data, is firmly established in the realm of reference types.\n\nThis feels upside down to me. If I were to choose to use value types in either model or view, I would be inclined to pick the model first. In fact, in the app [Agenda](https://agenda.com), we took exactly this approach: the model is made up of structs, and the view utilizes standard AppKit/UIKit types, _ie_, classes.\n\n## Values All the Way Down\n\nOf course, one choice need not exclude the other. Maybe the best solution is to use value types in both the view _and_ the model. It's this option I want to explore here by developing a basic contacts app with SwiftUI. But we'll take it a step further, not only using value types, but also adopting immutable data throughout, right down to the on disk storage.\n\nIf you want to try out this app without going to the trouble of building it, you can [add it to Test Flight](https://testflight.apple.com/join/CInn5xrF). \n\n![The LoCo App]({{site.baseurl}}/images/data-driven-swiftui/LoCo.png)\n\n\n## LLVS\n\nWe'll use the [Low-Level Versioned Store (LLVS)](https://github.com/mentalfaculty/LLVS) for app storage, because it is based entirely on immutable files, and syncs automatically via CloudKit. The full source code for the sample app (LoCo) is [in the LLVS project](https://github.com/mentalfaculty/LLVS/tree/master/Samples/LoCo-SwiftUI/LoCo-SwiftUI).\n\nThe easiest way to think about LLVS is as Git for your app. The concepts are completely analogous: \n\n- They both maintain a full history of versions\n- Data can be retrieved for any version\n- Data can be stored to create a new version based on any earlier version\n- History can be branched, and merged\n\nLLVS has the added advantage that it abstracts away all sync and networking code, so building a syncing app is as easy as pushing and pulling with Git.\n\n## Data Driven\n\nBecause LLVS has a full history of versions, and each version is immutable, our data handling becomes dramatically simpler. The state of the whole app can be derived from a single value: the current version. \n\nWe can use the Combine framework to monitor changes to the current version, and propagate the changes through the data source class, and into the views. All of the SwiftUI views are literally a function of that one single value.\n\nHere is the [relevant code](https://github.com/mentalfaculty/LLVS/blob/master/Samples/LoCo-SwiftUI/LoCo-SwiftUI/ContactsDataSource.swift) from the `ContactsDataStore` class:\n\n```swift\nfinal class ContactsDataSource: ObservableObject  {\n\n    let storeCoordinator: StoreCoordinator\n\n    private var contactsSubscriber: AnyCancellable?\n\n    init(storeCoordinator: StoreCoordinator) {\n        self.storeCoordinator = storeCoordinator\n        contactsSubscriber = storeCoordinator.currentVersionSubject\n            .receive(on: DispatchQueue.main)\n            .map({ self.fetchedContacts(at: $0) })\n            .assign(to: \\.contacts, on: self)\n    }\n    \n    ...\n    \n    @Published var contacts: [Contact] = []\n```\n\nThe `StoreCoordinator` class is our interface to LLVS; it manages the store for us, tracking the current version, and merging changes from other devices. The class has a `currentVersionSubject`, which we can subscribe to. \n\nAfter shunting to the main queue, we use a Combine `map` to convert the current version into a list of contacts for that version. The method `fetchContacts` handles this, querying the `StoreCoordinator` for values stored in the current version, and unpacking the data using the `Codable` protocol to create an array of our model type, `Contact`.\n\nAfter the current contacts are fetched they are assigned to the `contacts` property; because this is `@Published`, it triggers an update to the SwiftUI `View` types, reflecting the data for the user. All of this arises whenever the current version of the `StoreCoordinator` changes, whether due to a local edit, or new data from a remote device.\n\n## The Views\n\nThe view code is quite standard SwiftUI. We create a list of contacts in `ContactsView`.\n\n```swift\nstruct ContactsView : View {\n    @EnvironmentObject var dataSource: ContactsDataSource\n    \n    ...\n    \n    var body: some View {\n        NavigationView {\n            List {\n                ForEach(dataSource.contacts) { contact in\n                    ContactCell(contactID: contact.id)\n                        .environmentObject(self.dataSource)\n                }\n                ...\n            }\n        ...\n```\n\nThe `ContactsDataSource` object is passed in here as an `@EnvironmentObject`, and the `contacts` property from the previous section is used to generate the list cells.\n\nWhen the user taps a cell, a detail view is pushed onto the navigation stack, showing the details of the contact in a form.\n\n```swift\nstruct ContactView: View {\n    @EnvironmentObject var dataSource: ContactsDataSource\n    var contactID: Contact.ID\n    \n    ...\n    \n    var body: some View {\n        NavigationView {\n            Form {\n                Section(header: Text(\"Name\")) {\n                    TextField(\"First Name\", text: contact.person.firstName)\n                    TextField(\"Last Name\", text: contact.person.secondName)\n                }\n                Section(header: Text(\"Address\")) {\n                    TextField(\"Street Address\", text: contact.address.streetAddress)\n                    TextField(\"Postcode\", text: contact.address.postCode)\n                    TextField(\"City\", text: contact.address.city)\n                    TextField(\"Country\", text: contact.address.country)\n                }\n            }\n            .navigationBarTitle(Text(\"Contact\"))\n        }\n    }\n}\n```\n\nThis is what it looks like to the user.\n\n![Contact Detail View]({{site.baseurl}}/images/data-driven-swiftui/ContactDetails.png)\n\n\n## Change Without Mutation\n\nSo far, we have no mechanism to change the contacts data. This is where it gets more interesting, because we are going to update the contacts without actually mutating any of our data.\n\nLet's take the case of updating an existing contact. (Inserting and deleting are very similar.) We need a means to observe changes in the text fields of the `ContactView`. In SwiftUI, that usually means a binding. Here is the `contact` binding that we used to populate the form above.\n\n```swift\n    private var contact: Binding<Contact> {\n        Binding<Contact>(\n            get: { () -> Contact in\n                self.dataSource.contact(withID: self.contactID)\n            },\n            set: { newContact in\n                self.dataSource.update(newContact)\n            }\n        )\n    }\n```\n\nUsually a binding would be a wrapper around a simple value, but, in this case, the getter fetches the contact from the `ContactsDataSource`, and the setter calls an `update` method passing the changed contact.\n\n```swift\n    func update(_ contact: Contact) {\n        let change: Value.Change = .update(try! contact.encodeValue())\n        try! storeCoordinator.save([change])\n        sync()\n    }\n```\n\nAs you can see, the `update` method doesn't actually make any changes to the `contacts` array in `ContactsDataSource`, which is what you would probably expect it to do. Instead, it encodes the new value, and saves it straight into the LLVS store to create a new version.\n\nStop to think about that for a minute: we didn't actually mutate any of the data in our `ContactsDataSource`, or SwiftUI views. We simply created a new `Contact` value, and saved it straight to disk.\n\n\nIf we don't update the array of contacts in the data source class, how do edits end up on screen? Well, we saved the new value to the LLVS store, which causes the current version to change, and this induces the chain of observation we started with, updating the whole UI. The cycle is complete.\n\n![The Data Cycle]({{site.baseurl}}/images/data-driven-swiftui/DataFlow.png)\n\n\n## A Merge at Every Coal Face\n\nWho cares? Why is this useful? Here is something you realize when implementing sync in a non-trivial app: whenever you have a mutable copy of the data, you have a merge problem. \n\nFor example, imagine you fetch data from disk, and store it in a controller. What happens when new changes arrive from a different device? You have to merge those changes into the controller's data. And what happens when the user makes changes in the view? You have to merge those changes into the controller's copy of the data.\n\nAnd the same applies at every level of the app. If you are working on a view class, you have to be careful to pull updates in from the controller, and merge them with any changes the user has just made. In short, there is a merge problem at every coal face. Any mutable copy of your data is another merge problem to solve.\n\nThe reason the solution above works so well is that there is no mutable copy of the data. The only mutation occurs in the data store when changing the current version. All merging occurs in this step, via the mechanisms provided by LLVS. In this particular example, we have opted for a simple \"most recent value wins\" merge policy, but we could make merging as sophisticated as needed. We do this in one place, rather than throughout the app for every mutable copy of the data.\n\n## Early Adopters\n\nIt's still very early days for SwiftUI, but now is the time to start exploring new ways to build apps. Question your assumptions. The dam is ready to break.\n\nIn this post, we've seen how using a distributed store like LLVS can complement SwiftUI. You can build your whole app around immutable value types, and vastly simplify sync and data merging.\n\n"
  },
  {
    "path": "docs/index.md",
    "content": "## Posts\n\n<ul>\n  {% for post in site.posts %}\n    <li>\n      <a href=\"{{ post.url | prepend: site.baseurl  }}\">{{ post.title }}</a>\n    </li>\n  {% endfor %}\n</ul>\n"
  }
]