{"id":13995410,"url":"https://github.com/mentalfaculty/LLVS","last_synced_at":"2025-07-22T21:32:55.193Z","repository":{"id":63917760,"uuid":"210908515","full_name":"mentalfaculty/LLVS","owner":"mentalfaculty","description":"Low-Level Versioned Store","archived":false,"fork":false,"pushed_at":"2023-10-14T09:38:28.000Z","size":1505,"stargazers_count":208,"open_issues_count":0,"forks_count":9,"subscribers_count":15,"default_branch":"main","last_synced_at":"2024-07-24T15:02:39.957Z","etag":null,"topics":["distributed-storage","swift","swiftui","sync","synchronization"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mentalfaculty.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-09-25T18:00:57.000Z","updated_at":"2024-02-27T13:36:42.000Z","dependencies_parsed_at":"2024-01-20T17:58:16.026Z","dependency_job_id":"ad4afb52-ac30-4394-b7d0-a04258a2af72","html_url":"https://github.com/mentalfaculty/LLVS","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentalfaculty%2FLLVS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentalfaculty%2FLLVS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentalfaculty%2FLLVS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentalfaculty%2FLLVS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mentalfaculty","download_url":"https://codeload.github.com/mentalfaculty/LLVS/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":214675557,"owners_count":15768176,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-storage","swift","swiftui","sync","synchronization"],"created_at":"2024-08-09T14:03:23.433Z","updated_at":"2024-08-09T14:16:22.245Z","avatar_url":"https://github.com/mentalfaculty.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"[![](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)_\n\n## Introduction to LLVS\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 a tool like Git? If so, read on.\n\n### Why LLVS?\n\nApplication data is more decentralized than ever. It is rare to find it concentrated on a single device. Typically, a single user will have a multitude of devices, from phones to laptops and watches, and each device may have several independent processes running (eg today extension, sharing extension), each working with a copy of the data. How can you share all of this data, giving the user a consistent picture, without writing thousands of lines of custom code to handle each scenario?\n\nSoftware developers faced a similar situation in the world of source control. They were faced with this question: How can multiple individuals all work together on the same source files across multiple computers? We all know how that played out. Advances in Source Control Management (SCM) led to systems like Git and GitHub.\n\nThese tools successfully solved the problem of decentralized collaboration, and it is odd that the same approach has not been applied to app data before. That's where LLVS comes in. It provides a basic framework for storing and moving data through the decentralized world in which our apps live.\n\n### What is LLVS?\n\nLLVS is best described as a _decentralized, versioned, key-value storage framework_. \n\nIt works a bit like a traditional key-value store, in which you can insert data values for given unique keys. But LLVS adds to this several extra dimensions, namely, that when a set of values are stored, a new version is created in the store, and each version has an ancestry of other versions which you can trace back in time. Just as with Git, you can retrieve the values for any version at any time, determine the values that changed between any two versions, and merge together versions.\n\nAll of this would be great on its own, but if it were isolated to a single store, it still would not be very useful in our decentralized world. So LLVS can _send_ and _receive_ versions from other stores, in the same way that you _push_ and _pull_ from other repositories with Git.\n\nIf this still has you wondering what LLVS is about, here are a few other characterizations which may help you grok it. LLVS is...\n\n- A decentralized, versioned, key-value storage framework\n- An abstractive layer for handling the ancestry of a decentralized data set\n- A Directed, Acyclic Graph (DAG) history of the data in your app\n- A simple means to \"sync up\" data sets across devices, and in the cloud\n- An append-only distributed store, with full versioned history\n\n### What is LLVS _Not_?\n\nAt this point, you are probably trying to apply labels to LLVS; trying to categorize it in terms of what you are already acquainted with. Try to keep an open mind lest you miss important, atypical aspects of the framework. To help you in this direction, here is a list of things that LLVS is _not_:\n\n- An Object Relational Modeling (ORM) framework\n- A database\n- A serialization framework\n- A framework for developing web services\n\n### Where Does it Fit In?\n\nLLVS is an abstraction. It handles the history of a dataset, without needing to know what the data actually represents, how it is stored on disk, or even how it moves between devices. \n\nLLVS includes some classes to get you started. You can set up a basic store using the existing storage classes (eg file based), and distribute your data using an existing cloud service (eg CloudKit), but you could also choose to add support for your own store (eg SQLite) or cloud service (eg Firebase). And you are free to use any data format you like, including serialized Swift Codable values, JSON, and end-to-end encrypted formats.\n\n\n## Installing\n\n### Swift Package Manager (SPM) in Xcode 11 or later\n\nThe easiest way to get started with LLVS is using Xcode 11 or later, which supports the Swift Package Manager (SPM). \n\n1. To add LLVS, choose _File \u003e Swift Packages \u003e Add Package Dependency..._. \n2. Enter the LLVS repo URL: [https://github.com/mentalfaculty/LLVS](https://github.com/mentalfaculty/LLVS)\n3. Now enter the minimum version you require (eg 0.3.0), and add the package. \n4. Select your project in the Xcode source list on the left.\n5. Select your app target and go to the _General_ tab.\n6. Add the frameworks you need to your app target in the _Frameworks, Libraries, and Embedded Content_ (_e.g._ LLVS, LLVSCloudKit).\n\n### Swift Package Manager\n\nIf you aren't building with Xcode, you can instead add this to your SPM `Package.swift`. \n\n```\ndependencies: [\n    .package(url: \"https://github.com/mentalfaculty/LLVS.git, from: \"0.3.0\")\n]\n```\n\n### Manually with Xcode\n\nTo manually add LLVS to your Xcode project...\n\n1. Download the source code or add the project as a submodule with Git.\n2. Drag the _LLVS_ root folder into your own Xcode app project.\n3. Select your project in the source list, and then your app's target.\n4. Add the _LLVS_ framework in the _Frameworks, Libraries, and Embedded Content_ section of the _General_ tab.\n\n### Trying it Out First\n\nIf you don't want to go to the trouble of installing the framework, but do want to test it out in practice, you can try out the LoCo sample app via Test Flight. Use the link below to add the app to Test Flight on your iOS device.\n\n[https://testflight.apple.com/join/nMfzRxt4](https://testflight.apple.com/join/nMfzRxt4)\n\n\n## Quick Start\n\nThis section gets you up and running as fast as possible with a iOS app that syncs via CloudKit. \n\n### The Message\n\nWe are going to walk through a project called 'TheMessage', which you can find in the _Samples_ directory. It's about the simplest app you can envisage, which is exactly why it is useful for our purposes. \n\nA single message is shown on the screen. The user can edit the message if they choose. The message is saved into an LLVS store, and syncs via the CloudKit public database to anyone else using the app. So the message is shared between all users, and can be updated by any of them — they all share the same LLVS distributed store.\n\n### Setting Up the Project\n\nBefore we look at the code, let's walk through some aspects of setting up the project.\n\n#### Creating the Xcode Project\n\nTheMessage was generated as an Xcode project using the _Single View App_ template. We will be using SwiftUI for the view layer, so make sure that is checked.\n\n#### Adding LLVS\n\nWith a project in place, we now need to add LLVS. You can use any of the approaches in the section on Installing above. \n\nThe approach taken for the sample project was to drag the LLVS root folder into the Xcode project, and then add the frameworks to the target.\n\nThe frameworks we need for TheMessage are LLVS and LLVSCloudKit.\n\n#### Adding CloudKit\n\nNow that LLVS is in place, we need to setup CloudKit. \n\n1. Select TheMessage project in the source list.\n2. Select the app target, and the the _Signing \u0026 Capabilities_ tab.\n3. Press the + button, and add iCloud.\n4. Check the CloudKit checkbox.\n5. Add a container for the app. _E.g._ \"iCloud.com.yourcompany.themessage\"\n\n### Add a Store Coordinator\n\nMost of the source code resides directly in the _AppDelegate_ file. (Do not try this at home!) \n\nFirst, we have code to setup a `StoreCoordinator` object.\n\n```swift\nlazy var storeCoordinator: StoreCoordinator = {\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, \n        storeIdentifier: \"MainStore\", \n        cloudDatabaseDescription: .publicDatabase(container))\n    coordinator.exchange = exchange\n    return coordinator\n}()\n```\n\nA `StoreCoordinator` takes care of a lot of the mundane aspects of managing an LLVS store, such as tracking what version of the data your app is using, and merging data from other devices. It also makes saving and fetching data more convenient, so it is perfect for your first app.\n\nIn addition to creating the `StoreCoordinator`, the code above also sets up a `CloudKitExchange`, and attaches it to the coordinator. An `Exchange` is an object that can send and receive the store data; in this case, it is sending data to CloudKit so that other devices can add it to their local store, and receiving changes made by other devices from CloudKit.\n\n### Store Some Data\n\nThe `AppDelegate` includes the data for the message shown on screen, as well as functions to fetch it from the store, and save it to the store.\n\n```swift\nlet messageId = Value.ID(\"MESSAGE\") // Id in the store\n@Published var message: String = \"\"\n```\n\nThe message has an identifier in LLVS, called a _value identifier_. Value identifiers are the _keys_ in the LLVS key-value store. They uniquely identify a value in the store.\n\nAs you can see, we declare a single fixed identifier for our message, of the type `Value.ID`. It is set to the string \"MESSAGE\", but the actual value is arbitrary. We need it to be the same on all devices, but it could be any string. As long as we end up with a single identifier, so that all users are updating the same message.\n\nStoring data in LLVS is handled by the `post` function.\n\n```swift\n/// Update the message in the store, and sync it to the cloud\nfunc 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\nLLVS stores `Value`s, which have an identifier (_ie_ key), and contain some data. So whatever data we use in our app needs to be converted into `Data` to store in `Value`s. In this case, where the message is just a `String`, this is trivial. In more advanced apps, you would likely use `Codable` types.\n\nTo save with the `StoreCoordinator`, we simply pass in an array of values we are updating. If needed, we could also pass values to insert and remove. (Note that updating will insert the value if the value is not yet in the store.)\n\nAfter updating the value, there is a sync, to ensure the data is uploaded to CloudKit. This is discussed more below.\n\n### Fetch Data\n\nFetching the message from the LLVS store is just as simple.\n\n```swift\n/// Fetch the message for the current version from the store\nfunc fetchMessage() -\u003e String? {\n    guard let value = try? storeCoordinator.value(id: messageId) else { return nil }\n    return String(data: value.data, encoding: .utf8)\n}\n```\n\nThe `value(id:)` func of `StoreCoordinator` will return a value if it exists in the store, and `nil` otherwise. The `fetchMessage` func uses this to try to get the message value, with the same message identifier that we use in the save. If it is found, the data is extracted from the value and converted into the message `String`; if not found, the function returns `nil`.\n\n### Sync\n\nSyncing with LLVS is also very simple.\n\n```swift\nfunc sync() {\n    // Exchange with the cloud\n    storeCoordinator.exchange { _ in\n        // Merge branches to get the latest version\n        self.storeCoordinator.merge()\n    }\n}\n```\n\nNo networking code or complex CloudKit operations needed. Just call `exchange` to send and receive any changes with the cloud, and then call `merge` to resolve any changes made between devices.\n\n### Ship It!\n\nThe rest of the code is generic SwiftUI, and won't be covered here. All up, The Message is less than 200 lines of code. Sure, it doesn't do very much, but as we have seen above, saving, fetching, and even syncing can be achieved in just a few lines.\n\nIf we were to distribute The Message via the App Store, we would first need go to the [CloudKit Dashboard](http://icloud.developer.apple.com) and make sure the development schema gets deployed to production. \n\n\n## Advanced: The `Store` Class\n\nThe Quick Start makes use of a class called `StoreCoordinator`, which simplifies using LLVS a lot. You can ignore many of the internal details, like versions, branching, and merging. This is a good way to begin using the framework, and for many apps will be all you need.\n\nBut LLVS has a lot more to offer. It gives you full access to the history of your data. You can fetch data for any version, compare changes between versions, and apply powerful merging algorithms to create new versions.\n\nAll of this is available via the core class of LLVS: `Store`. \n\n### Creating a Store\n\nA `StoreCoordinator` wraps around a `Store`, which is accessible via the `store` property, but you can also completely ignore `StoreCoordinator` and use `Store` directly. `StoreCoordinator` is just a convenience — you can do it all with `Store`.\n\nCreating a versioned store on a single device is as simple as passing in a directory URL.\n\n```swift\nlet groupDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.com.mycompany.myapp\")!\nlet rootDir = groupDir.appendingPathComponent(\"MyStore\")\nlet store = Store(rootDirectoryURL: rootStoreDirectory)\n```\n\nThis code uses the app group container, which is useful if you want to share data with app extensions. (LLVS stores can be shared directly between multiple processes, such as the main app and a sharing extension.)\n\n### Inserting Values\n\nWhen you first create a store, you will probably want to add some initial data.\n\n```swift\nlet v1 = Value(idString: \"ABCDEF\", data: \"First Value\".data(using: .utf8)!)\nlet firstVersion = try store.makeVersion(basedOnPredecessor: nil, inserting: [v1])\n```\n\nThis code makes a new version in the store, inserting a new value. \n\nThe argument for `basedOnPredecessor` is `nil`, which indicates this is the first version in the store — there are no predecessor versions. (For Git users, this is the same as an initial commit.) \n\nUsually, you would might have a number of values to insert, update, and remove. All of this can be handled in a single call to `makeVersion`. The version made applies to the whole store, not just the values being manipulated.\n\nIf you are using `StoreCoordinator`, it will take care of storing the new version that is created; if you are using `Store` directly, it is up to you to follow which version you consider to be the current version for your app's UI. Typically, you would store it somewhere (_eg_ a variable, a file, user defaults), so that it can be used in fetching data. \n\n### Updating Values\n\nStoring subsequent values is very similar. The only difference is that you need to pass in the version upon which the changes are based, which is generally the current version being used by your app.\n\n```swift\nlet v2 = Value(idString: \"CDEFGH\", data: \"My second data\".data(using: .utf8)!)\nlet secondVersion = try store.makeVersion(basedOnPredecessor: firstVersion.id, inserting: [v2])\n```\n\nThe main difference here is that a non-nil predecessor is passed in when making the version. The predecessor is the identifier of the first version we created above, _ie_, `firstVersion.id`.\n\n### Versions are Store-Wide\n\nIt is important to realize that versions apply to the store as a whole. They are global, and form a complete history of the store, just like they do in a system like Git. Once a value has been added, it remains in existence, until it is removed or updated in a future version.\n\n### Fetching Data\n\nOnce we have data in the store, we can retrieve it again. With LLVS you need to indicate which version of the data you want, because the store includes a complete history of changes.\n\n```swift\nlet value = try store.value(idString: \"CDEFGH\", at: secondVersion.id)!\nlet fetchedString = String(data: value.data, encoding: .utf8)\n```\n\nHere we have fetched the second value we added above, and converted it back into a string. We passed in the second version identifier; if we had passed in the first version, `nil` would have been returned, because the value had not yet been added in that version.\n\nWhat about the first value we added above? That was added before the second version, so it continues to exist in future versions. So we can fetch it in exactly the same way. \n\n```swift\nlet valueId = Value.ID(\"ABCDEF\")\nlet value = try store.value(id: valueId, at: secondVersion.id)!\nlet fetchedString = String(data: value.data, encoding: .utf8)\n```\n\nWe've used a slightly different `value` func for this call, which takes the value identifier instead of a string, but the end result is the same.\n\n### Updating and Removing Values\n\nJust as you can _insert_ new values, you can also _update_ existing values, and _remove_ them.\n\n```swift\nlet updateData = \"An update of my first data\".data(using: .utf8)!\nlet updateValue = Value(idString: \"ABCDEF\", data: updateData)\nlet removeId = Value.ID(\"CDEFGH\")\nlet thirdVersion = try store.makeVersion(basedOnPredecessor: secondVersion.id, \n    updating: [updateValue], removing: [removeId])\n```\n\nThe third version is based on the second one. There are two changes: it updates the value for \"ABCDEF\" with new data, and removes the value for \"CDEFGH\". \n\nIf we now attempted to fetch the value with identifier \"CDEFGH\", we get `nil`.\n\n```swift\n// This will be nil\nlet value = try store.value(id: Value.ID(\"CDEFGH\"), at: thirdVersion.id)\n```\n\n### Branching\n\nIf the history of changes is _serial_ — one set of changes always based on the preceding — it is easy to work with your data. It gets more complex when _concurrent_ changes are made. If two versions are added at about the same time, you can end up with divergence of your data, and this will likely need to be merged at a later time.\n\nAn example of this is when a user makes changes on two different devices, with no interceding sync. Later, when the data does get transferred, the versions branch off, rather than appearing in a continuous ancestral line. This can even happen if you are not using sync; for example, if you have a sharing extension, and it adds a version while your main app is also adding a version.\n\n### Predecessors, Successors, and Heads\n\nThe `Version` type forms the basis of the history tracking in LLVS. A version can have up to two _predecessor_  versions upon which it is based, and it can have zero or more _successors_.\n\nA _head_ is a version that has no successors: there are no versions based off of the head version. They form the tips of branches; if you imagine the version history as a tree, with the bottom rooted at the initial version, the heads form the tips of branches at the top.\n\nHeads are important, because they usually represent recent changes. Most of the time, your app will use a head as the current version, and will base new versions off of a head.\n\nHeads are also important because, if there are more than one, they generally need to be merged together to create a single head version for the app to use.\n\n### Navigating History\n\nThe `History` class is used to query the history of the store. For example, you can ask for the heads at any time like so...\n\n```swift\nvar heads: Set\u003cVersion.Identifier\u003e?\nstore.queryHistory { history in\n    heads = history.headIdentifiers\n}\n```\n\nThe `queryHistory` function gets you access to the store's history object. You need to provide it with a block, which will be called synchronously. The reason the history is not vended as a simple property is to control access from different threads. Using the block allows access to the history to be serialized, so it can't change while you are querying it.\n\nOnce you have the `History` object, you can request the heads, but you can also retrieve any version you choose.\n\nGetting the most recent head is also easy.\n\n```swift\nlet version: Version? = store.mostRecentHead\n```\n\nThe most recent head is a convenient version to use when starting up an extension, or syncing for the first time on a new device. In effect, you are saying \"take me to the newest data\".\n\n### Merging\n\nOne of the strengths of LLVS is that it gives you a systematic way to resolve discrepancies between versions. If two devices each edit a particular value at about the same time, you can not only identify the _conflict_, but can merge the two disparate data values together into a new consistent version.\n\nThere are two types of merges in LLVS: two-way and three-way. Most of the time you will deal with three-way merges. A three way merge involves two versions — usually heads — and one so-called _common ancestor_. A common ancestor is a version that exists in the ancestry of each of the two versions being merged; it is a point in the history where the two versions were in agreement, before they diverged.\n\nMerging in this way is much more powerful than the facilities you get from most data modelling frameworks, because you don't just know what the latest values are, you also know what they were in the past, so you can determine what has changed.\n\nLLVS supports a second type of merge: two-way. This happens when more than one data set is added to the store that does not have any predecessors. Effectively, you have two initial versions. \n\nYou can get started with merging quite easily. \n\n```swift\nlet arbiter = MostRecentChangeFavoringArbiter()\nlet newVersion = try! store.merge(currentVersionId, with: otherHeadId, resolvingWith: arbiter)\n```\n\nIf it turns out that the version for `otherHeadId` is a descendent of the current version, this will not actually add a new version, but will just return the new head. (In Git terminology, it will _fast forward_.)\n\nHowever, normally the two versions will be divergent, and will need a three-way merge. The merge method will search back through the history, and find a common ancestor. It will then determine the differences between the two new versions by comparing them to the common ancestor. These differences will be passed to a `MergeArbiter` object, whose task it is to resolve any conflicts.\n\nIn this instance, we have used a built-in arbiter class called `MostRecentChangeFavoringArbiter`. As the name suggests, it will choose the most recently changed value whenever there is a conflict.\n\nIn your app, you are more likely to create your own arbiter class, to merge your data in custom ways. You can also choose to handle certain specific cases, and pass more standard tasks off to one of the existing arbiter classes.\n\n\n### Inside a MergeArbiter\n\nYou might be wondering what the internals of an arbiter class look like. It's typically a loop over the differences, treating each type of conflict. One of the simplest is `MostRecentBranchFavoringArbiter`, which will simply favor the branch that has the most recent timestamp. Here is the whole class.\n\n```swift\npublic class MostRecentBranchFavoringArbiter: MergeArbiter {\n    \n    public init() {}\n    \n    public func changes(toResolve merge: Merge, in store: Store) throws -\u003e [Value.Change] {\n        let v = merge.versions\n        let favoredBranch: Value.Fork.Branch = v.first.timestamp \u003e= 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(valueId, at: favoredVersion.id)!\n                    changes.append(.preserve(value.reference!))\n                }\n            case .twiceInserted, .twiceUpdated:\n                let value = try store.value(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\nThe engine of this class is the loop over _forks_. A fork summarizes the changes made in each branch for a single value identifier. Forks can be non-conflicting, like _.inserted_, _.removed_, _.updated_, and _.twiceRemoved_. These types involve either a change on a single branch, or a change on both branches that can be considered equivalent (_eg_ removing the value on both branches).\n\nAlternatively, a fork can be conflicting. At a minimum, the arbiter is required to return new changes for any forks that are conflicting. This is how they _resolve_ the conflicts in the merge. They can return a completely new change to resolve a conflicting fork, or they can _preserve_ an existing change. \n\nYou can see in the code above that when data is inserted on each branch, or updated on each branch, the arbiter _preserves_ the value from the more recent branch. When a _.removedAndUpdated_ is encountered — one branch removing the value, and another applying an update — the arbiter again preserves whichever change was made on the most recent branch.\n\nYou need not worry much about arbiters when getting started. You can just choose one of the existing classes, and start with that. Later, as you need more control, you can think about developing your own custom class that conforms to the `MergeArbiter` protocol.\n\n\n### Setting Up an Exchange\n\nLLVS is a decentralized storage framework, so you need a way to move versions between stores. For this purpose, we use an _Exchange_. An Exchange is a class that can send and receive data with the goal of moving it to/from other stores. \n\nCloudKit is a good choice for transferring data on Apple platforms. The `CloudKitExchange` class can be used to move data between LLVS stores using CloudKit.\n\nTo get started, we create the exchange.\n\n```swift\nself.cloudKitExchange = CloudKitExchange(with: store, storeIdentifier: \"MyStore\", cloudDatabaseDescription: .privateDatabaseWithCustomZone(CKContainer.default(), zoneIdentifier: \"MyZone\"))\n```\n\nTo retrieve new versions from the cloud, we simply call the `retrieve` func, which is asynchronous, with a completion callback.\n\n```swift\nself.cloudKitExchange.retrieve { result in\n    switch result {\n    case let .failure(error):\n        // Handle failure\n    case let .success(versionIds):\n        // Handle success\n    }\n}\n```\n\nSending new versions to the cloud is just as easy.\n\n```swift\nself.cloudKitExchange.send { result in\n    switch result {\n    case let .failure(error):\n        // Handle failure\n    case .success:\n        // Handle success\n    }\n}\n```\n\nLLVS has no limit on how many exchanges you setup. You can setup several for a single store, effectively pushing and pulling data via different routes. \n\nExchanges are also not limited to cloud services. You can write your own peer-to-peer class which conforms to the `Exchange` protocol. LLVS even includes `FileSystemExchange`, which is an exchange that works via a directory in the file system. This is very useful for testing your app without having to use the cloud.\n\n\n## Learning More\n\nThe best way to see how LLVS works in practice is to look in the provided Samples. There are very simple examples like TheMessage, but also more advanced apps like Loco-SwiftUI, a contact book app.\n\nThere are also useful posts at the [LLVS Blog](https://mentalfaculty.github.io/LLVS/).\n\n\n## How to Structure Your Data\n\nIf you are looking into the sample code, bear in mind that LLVS places no restrictions on what data you put into it. It is entirely up to you how you structure your app data. LLVS gives you a means to store and move the data around, and to track how it is changing, but the data itself is opaque to the framework.\n\nSomething to consider is the granularity of the data you put in each `Value`. You can go very fine grained, putting every single property of your model types into a separate `Value`, or you can go the other extreme and put all the data in a single `Value`. A good middle ground is to put each entity in the model (_eg_ struct or class) into a `Value`.\n\nThere are tradeoffs to each of these:\n\n- The fine grained, property level approach...\n    - Works well for merging, allowing each property to be merged independently\n    - Leads to slower loading, because each property must be fetched separately\n    - Results in many small files on disk, each containing a single property\n- The whole store in one `Value` approach...\n    - Can give fast loading, because only a single file is read\n    - Could lead to large disk and cloud use, as each version will add a new file with all values\n    - Will generally require manual merging of all data\n- The one entity per `Value` approach...\n    - Works reasonably well for merging, merging each entity independently\n    - Loads faster than property level storage\n    - Results in moderate file numbers, and loading times\n    \n    \nOur advice for getting started is to use the _one entity per value_ approach — it's Goldilocks.\n\n## Features of LLVS\n\nHere are list of LLVS features, some of which may not be apparent from the description above.\n\n- No lock in! Use multiple services to exchange data, and switch at will\n- Includes a full history of changes\n- Branch and merge history\n- Determine what changed between two different versions\n- Work with old versions of data\n- Playback changes\n- Revert commits for undo purposes\n- Extend to any cloud storage\n- Support pure peer-to-peer exchange\n- Support end-to-end encryption\n- Add support for any on-disk storage (_eg_ SQLite, CouchDB, flat file)\n- Systematic 3-way merging of data\n- Append only, so very robust. No mutable data\n- Fully thread safe. Work from any thread, and switch at any time\n- Multiprocess safe. Share the same store between processes (eg extensions)\n- Can safely be added to syncing folders like Dropbox (unlike SQLite, for example)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmentalfaculty%2FLLVS","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmentalfaculty%2FLLVS","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmentalfaculty%2FLLVS/lists"}