Showing preview only (569K chars total). Download the full file or copy to clipboard to get everything.
Repository: mentalfaculty/LLVS
Branch: main
Commit: 92fe81b57dc5
Files: 103
Total size: 535.0 KB
Directory structure:
gitextract_ytf8b50v/
├── .gitignore
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENCE.txt
├── Package.swift
├── README.md
├── Samples/
│ ├── LoCo/
│ │ ├── LoCo/
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AccentColor.colorset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── Contact.swift
│ │ │ ├── ContactStore.swift
│ │ │ ├── ContactView.swift
│ │ │ ├── ContactsView.swift
│ │ │ ├── LoCo.entitlements
│ │ │ ├── LoCoApp.swift
│ │ │ └── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ └── LoCo.xcodeproj/
│ │ └── project.pbxproj
│ └── TheMessage/
│ ├── TheMessage/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── ContentView.swift
│ │ ├── MessageStore.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ ├── TheMessage.entitlements
│ │ └── TheMessageApp.swift
│ └── TheMessage.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── Sources/
│ ├── LLVS/
│ │ ├── Core/
│ │ │ ├── Exchange.swift
│ │ │ ├── FileZone.swift
│ │ │ ├── FolderBasedExchange.swift
│ │ │ ├── History.swift
│ │ │ ├── Map.swift
│ │ │ ├── Merge.swift
│ │ │ ├── Snapshot.swift
│ │ │ ├── SnapshotCapable+ZIP.swift
│ │ │ ├── Storage.swift
│ │ │ ├── Store.swift
│ │ │ ├── StoreCoordinator.swift
│ │ │ ├── Value.swift
│ │ │ ├── Version.swift
│ │ │ └── Zone.swift
│ │ ├── Exchanges/
│ │ │ ├── CloudFileSystem.swift
│ │ │ ├── CloudFileSystemExchange.swift
│ │ │ ├── FileSystemExchange.swift
│ │ │ ├── MemoryExchange.swift
│ │ │ └── MultipeerExchange.swift
│ │ ├── General/
│ │ │ ├── Cache.swift
│ │ │ ├── DataCompression.swift
│ │ │ ├── DynamicTaskBatcher.swift
│ │ │ ├── General.swift
│ │ │ └── Log.swift
│ │ └── Utilities/
│ │ └── ArrayDiff.swift
│ ├── LLVSBox/
│ │ └── BoxExchange.swift
│ ├── LLVSCloudKit/
│ │ └── CloudKitExchange.swift
│ ├── LLVSGoogleDrive/
│ │ ├── GoogleDriveAuthenticator.swift
│ │ └── GoogleDriveFileSystem.swift
│ ├── LLVSModel/
│ │ ├── Macros.swift
│ │ ├── Mergeable.swift
│ │ ├── MergeableArbiter.swift
│ │ ├── StorableModel.swift
│ │ └── StoreCoordinator+Model.swift
│ ├── LLVSModelMacros/
│ │ ├── MergeableModelMacro.swift
│ │ └── Plugin.swift
│ ├── LLVSOneDrive/
│ │ ├── OneDriveAuthenticator.swift
│ │ └── OneDriveFileSystem.swift
│ ├── LLVSPCloud/
│ │ └── PCloudExchange.swift
│ ├── LLVSSQLite/
│ │ ├── SQLiteDatabase+Zones.swift
│ │ ├── SQLiteDatabase.swift
│ │ └── SQLiteZone.swift
│ ├── LLVSWebDAV/
│ │ ├── WebDAVFileSystem.swift
│ │ └── WebDAVResponseParser.swift
│ └── SQLite3/
│ ├── module.modulemap
│ └── shim.h
├── Tests/
│ ├── LLVSModelTests/
│ │ └── MergeableArbiterTests.swift
│ └── LLVSTests/
│ ├── ArrayDiffTests.swift
│ ├── CloudFileSystemExchangeTests.swift
│ ├── DataCompressionTests.swift
│ ├── DiffTests.swift
│ ├── DynamicTaskBatcherTests.swift
│ ├── FileSystemExchangeTests.swift
│ ├── FileZoneTests.swift
│ ├── GeneralTests.swift
│ ├── HistoryTests.swift
│ ├── MapTests.swift
│ ├── MemoryExchangeTests.swift
│ ├── MergeTests.swift
│ ├── MostRecentBranchArbiterTests.swift
│ ├── MostRecentChangeArbiterTests.swift
│ ├── MultipeerExchangeTests.swift
│ ├── PerformanceTests.swift
│ ├── PrevailingValueTests.swift
│ ├── SQLitePerformanceTests.swift
│ ├── SQLiteZoneTests.swift
│ ├── SerialHistoryTests.swift
│ ├── SharedStoreTests.swift
│ ├── SnapshotTests.swift
│ ├── StoreSetupTests.swift
│ ├── ValueChangesInVersionTests.swift
│ ├── ValueTests.swift
│ └── VersionTests.swift
└── docs/
├── _config.yml
├── _posts/
│ └── 2019-09-25-data-driven-swiftui.md
└── index.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/.swiftpm
**/Package.resolved
/Packages
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
.claude/
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is LLVS?
LLVS (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.
## Build & Test Commands
```bash
swift build # Build all targets
swift test # Run all 170 tests
swift test --filter LLVSTests.StoreSetupTests # Run a single test class
swift test --filter testStoreCreatesDirectories # Run a single test method by name
```
Tests 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.
## Package Structure
SPM targets with a layered dependency graph:
- **LLVS** — Core framework, zero dependencies. All fundamental types and logic.
- **LLVSModel** — High-level model layer with `@MergeableModel` macro, `StorableModel` protocol, and `MergeableArbiter`. Depends on LLVS.
- **LLVSModelMacros** — Swift macro implementation for `@MergeableModel`. Depends on SwiftSyntax.
- **LLVSSQLite** — SQLite storage backend. Depends on LLVS and SQLite3.
- **LLVSCloudKit** — CloudKit sync exchange. Depends on LLVS.
- **SQLite3** — System library wrapper for SQLite.
## Architecture
### Core Data Flow
`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.
`StoreCoordinator` wraps `Store` with convenience: it tracks the "current version" for the app UI, simplifies save/fetch, and orchestrates exchange + merge cycles.
### Version History (DAG)
Versions 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 ... }`.
### Merging
Three-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.
`@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.
`Value.Fork` describes per-value conflict states: `.inserted`, `.updated`, `.removed` (non-conflicting, single branch), `.twiceInserted`, `.twiceUpdated`, `.removedAndUpdated` (conflicting, require arbiter resolution).
### Storage Abstraction
`Storage` protocol creates `Zone` instances. `Zone` is the raw read/write interface (`store(_:for:)` / `data(for:)`). Two implementations:
- `FileZone` — hierarchical files on disk under the store's root directory. Uses 2-char prefix subdirectories for filesystem efficiency. Multi-process safe.
- `SQLiteZone` — SQLite-backed storage via `LLVSSQLite`. Not thread-safe by design (caller manages concurrency).
### Sync (Exchange)
`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:
- `CloudKitExchange` — syncs via CloudKit (private, public, or shared databases).
- `FileSystemExchange` — syncs via a shared filesystem directory (useful for testing).
- `MemoryExchange` — actor-based in-memory exchange.
- `MultipeerExchange` — peer-to-peer exchange using `PeerTransport` protocol.
### Map (Value Index)
`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.
### Key Value Types
- `Value` — has an `ID` (string key) and `Data` payload, plus an optional `Reference` (version + key) for locating stored data.
- `Value.Change` — enum: `.insert`, `.update`, `.remove`, `.preserve`, `.preserveRemoval`. These are what get stored per-version.
- `Version.ID` — wrapper around a UUID string.
- `Branch` — wrapper around a raw string, stored in version metadata.
================================================
FILE: CONTRIBUTING.md
================================================
### Low-Level Versioned Store (LLVS) – Contributor License Agreement (v1)
Thank 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.
You 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.
1. Definitions.
"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.
"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."
2. 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.
3. 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.
4. 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.
5. 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.
6. 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.
7. 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]".
8. 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.
================================================
FILE: LICENCE.txt
================================================
Copyright (c) 2019 Drew McCormack
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "LLVS",
platforms: [
.macOS(.v15), .iOS(.v18), .watchOS(.v11)
],
products: [
.library(
name: "SQLite3",
targets: ["SQLite3"]),
.library(
name: "LLVS",
targets: ["LLVS"]),
.library(
name: "LLVSCloudKit",
targets: ["LLVSCloudKit"]),
.library(
name: "LLVSSQLite",
targets: ["LLVSSQLite"]),
.library(
name: "LLVSPCloud",
targets: ["LLVSPCloud"]),
.library(
name: "LLVSBox",
targets: ["LLVSBox"]),
.library(
name: "LLVSModel",
targets: ["LLVSModel"]),
.library(
name: "LLVSWebDAV",
targets: ["LLVSWebDAV"]),
.library(
name: "LLVSGoogleDrive",
targets: ["LLVSGoogleDrive"]),
.library(
name: "LLVSOneDrive",
targets: ["LLVSOneDrive"]),
],
dependencies: [
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")),
.package(url: "https://github.com/pCloud/pcloud-sdk-swift.git", from: "3.0.0"),
.package(url: "https://github.com/box/box-ios-sdk.git", from: "10.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"),
],
targets: [
.systemLibrary(
name: "SQLite3"
),
.target(
name: "LLVS",
dependencies: [
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
],
swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(
name: "LLVSTests",
dependencies: ["LLVS", "LLVSSQLite"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSCloudKit",
dependencies: ["LLVS"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSSQLite",
dependencies: ["LLVS", "SQLite3"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSPCloud",
dependencies: [
"LLVS",
.product(name: "PCloudSDKSwift", package: "pcloud-sdk-swift")
],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSBox",
dependencies: [
"LLVS",
.product(name: "BoxSDK", package: "box-ios-sdk")
],
swiftSettings: [.swiftLanguageMode(.v5)]),
.macro(
name: "LLVSModelMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]),
.target(
name: "LLVSModel",
dependencies: [
"LLVS",
"LLVSModelMacros",
],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSWebDAV",
dependencies: ["LLVS"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSGoogleDrive",
dependencies: ["LLVS"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "LLVSOneDrive",
dependencies: ["LLVS"],
swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(
name: "LLVSModelTests",
dependencies: [
"LLVSModel",
"LLVS",
"LLVSSQLite",
],
swiftSettings: [.swiftLanguageMode(.v5)])
]
)
================================================
FILE: README.md
================================================
[](https://swiftpackageindex.com/mentalfaculty/LLVS)
[](https://swiftpackageindex.com/mentalfaculty/LLVS)
# Low-Level Versioned Store (LLVS)
_Author: Drew McCormack ([@drewmccormack](https://github.com/drewmccormack))_
Ever wish it was as easy to move your app's data around as it is to push and pull your source code with Git?
LLVS 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.
### The problem
A 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.
Keeping 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.
LLVS 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.
### What you get
- **Version history** -- Every save is a version. Branch, merge, diff, or revert to any point in time.
- **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.
- **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.
- **Multi-process safe** -- Share a store between your main app, extensions, and widgets using an app group container. LLVS handles concurrent access.
- **Pluggable storage** -- File-based storage by default, SQLite via `LLVSSQLite`, or bring your own backend.
- **Encryption-friendly** -- LLVS stores opaque `Data` blobs. Encrypt them however you want; the framework never inspects your data.
## Quick Start
This walks through a minimal app that syncs a shared message via CloudKit. Full code is in _Samples/TheMessage_.
### Set up a StoreCoordinator
`StoreCoordinator` is the simplest entry point. It wraps a `Store`, tracks the current version, and orchestrates sync and merging.
```swift
lazy var storeCoordinator: StoreCoordinator = {
let coordinator = try! StoreCoordinator()
let container = CKContainer(identifier: "iCloud.com.mycompany.themessage")
let exchange = CloudKitExchange(
with: coordinator.store,
storeIdentifier: "MainStore",
cloudDatabaseDescription: .publicDatabase(container)
)
coordinator.exchange = exchange
return coordinator
}()
```
### Save, fetch, sync
```swift
let messageId = Value.ID("MESSAGE")
func post(message: String) {
let value = Value(id: messageId, data: message.data(using: .utf8)!)
try! storeCoordinator.save(updating: [value])
sync()
}
func fetchMessage() -> String? {
guard let value = try? storeCoordinator.value(id: messageId) else { return nil }
return String(data: value.data, encoding: .utf8)
}
func sync() {
storeCoordinator.exchange { _ in
self.storeCoordinator.merge()
}
}
```
`exchange` sends and receives versions with the cloud. `merge` reconciles any concurrent changes. That's the entire sync implementation.
This example is deliberately minimal -- the real power of LLVS shows up when data diverges across devices, which is covered below.
## Installation
### Swift Package Manager
Add LLVS as a dependency in your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/mentalfaculty/LLVS.git", from: "0.3.0")
]
```
Then add the libraries you need: `LLVS` for the core framework, `LLVSSQLite` for SQLite-backed storage, and `LLVSCloudKit` for CloudKit sync.
### Xcode
Choose _File > Add Package Dependencies..._, enter the LLVS repository URL, and select the libraries your target needs.
### Platforms
macOS 10.15+, iOS 13+, watchOS 6+.
## Working with `Store`
`StoreCoordinator` is convenient for common cases, but `Store` gives you direct access to the version graph -- branching, merging, diffing, and time travel.
### Creating a Store
```swift
let rootDir = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp")!
.appendingPathComponent("MyStore")
let store = try Store(rootDirectoryURL: rootDir)
```
Using an app group container lets your main app, extensions, and widgets share the same store. For SQLite-backed storage:
```swift
let store = try Store(rootDirectoryURL: rootDir, storage: SQLiteStorage())
```
### Versions and values
Every write creates a new version:
```swift
let value = Value(idString: "ABCDEF", data: "Hello".data(using: .utf8)!)
let firstVersion = try store.makeVersion(basedOnPredecessor: nil, inserting: [value])
```
Passing `nil` for the predecessor creates the initial version -- like Git's first commit. Subsequent changes build on a predecessor:
```swift
let updated = Value(idString: "ABCDEF", data: "World".data(using: .utf8)!)
let secondVersion = try store.makeVersion(
basedOnPredecessor: firstVersion.id,
updating: [updated]
)
```
Inserts, updates, and removes can be combined in a single call:
```swift
let thirdVersion = try store.makeVersion(
basedOnPredecessor: secondVersion.id,
inserting: [newValue],
updating: [changedValue],
removing: [obsoleteValueId]
)
```
Versions 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:
```swift
let value = try store.value(idString: "ABCDEF", at: secondVersion.id)!
```
### Branching and heads
When 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.
Each `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.
```swift
store.queryHistory { history in
let heads = history.headIdentifiers
// ...
}
// Or get the most recent head directly:
let latest: Version? = store.mostRecentHead
```
### Merging
This 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.
```swift
let arbiter = MostRecentChangeFavoringArbiter()
let merged = try store.merge(version: headA, with: headB, resolvingWith: arbiter)
```
If one version is an ancestor of the other, LLVS fast-forwards without creating a new version -- just like Git.
LLVS ships with two built-in arbiters:
- `MostRecentChangeFavoringArbiter` -- resolves each conflict individually by keeping whichever change is newer.
- `MostRecentBranchFavoringArbiter` -- resolves all conflicts by favoring whichever branch has the newer timestamp.
For full control, implement the `MergeArbiter` protocol:
```swift
public protocol MergeArbiter {
func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change]
}
```
The `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.
### Sync (Exchange)
An `Exchange` sends and receives versions between stores -- the equivalent of `git push` and `git pull`.
**CloudKit** (via the `LLVSCloudKit` library):
```swift
let exchange = CloudKitExchange(
with: store,
storeIdentifier: "MyStore",
cloudDatabaseDescription: .privateDatabaseWithCustomZone(
CKContainer.default(), zoneIdentifier: "MyZone"
)
)
```
**File system** (useful for testing and inter-process sync):
```swift
let exchange = FileSystemExchange(
rootDirectoryURL: sharedDirectoryURL, store: store
)
```
Retrieving and sending are both asynchronous:
```swift
exchange.retrieve { result in /* handle result */ }
exchange.send { result in /* handle result */ }
```
You 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.
## Structuring Your Data
LLVS stores opaque `Data` blobs keyed by string identifiers. How you map your model onto values is up to you, but the granularity matters:
| Approach | Merging | Performance | Disk use |
|---|---|---|---|
| **One property per Value** | Best (per-property conflict resolution) | Slow (many small reads) | Many small files |
| **One entity per Value** | Good (per-entity conflict resolution) | Moderate | Moderate |
| **Entire model in one Value** | Poor (must merge everything manually) | Fast (single read) | Large per-version files |
**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.
## Snapshots
When 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.
**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.
### Bootstrapping a new device
```swift
let coordinator = try StoreCoordinator(
withStoreDirectoryAt: storeURL,
cacheDirectoryAt: cacheURL,
snapshotPolicy: .auto
)
coordinator.exchange = myExchange
// On first launch, try to restore from a snapshot before syncing
coordinator.bootstrapFromSnapshot { error in
coordinator.exchange { _ in
coordinator.merge()
}
}
```
`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.
### Automatic snapshot uploads
With `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.
### Custom storage and exchange support
Snapshot support requires both the storage backend and the exchange to opt in:
- **Storage**: Conform to `SnapshotCapable` (both `FileStorage` and `SQLiteStorage` already do).
- **Exchange**: Conform to `SnapshotExchange` (`FileSystemExchange` already does).
If either side doesn't conform, snapshot operations are silently skipped.
## Architecture
This section covers the internal design for contributors and anyone who wants to understand what's happening under the hood.
### Package structure
LLVS is split into four SPM targets:
| Target | Purpose | Dependencies |
|---|---|---|
| **LLVS** | Core framework: `Store`, `History`, `Map`, `Zone`, `Exchange`, `Value`, `Version` | None |
| **LLVSSQLite** | SQLite storage backend | LLVS, SQLite3 |
| **LLVSCloudKit** | CloudKit sync exchange | LLVS |
| **SQLite3** | System library wrapper | System SQLite |
### Core data flow
`Store` is the central class. It owns three components:
- **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`.
- **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.
- **Zone** -- A pluggable storage backend for reading and writing raw data.
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 walks the map to find where a value's data is physically stored.
### Storage abstraction
The `Storage` protocol creates `Zone` instances. `Zone` is the raw read/write interface with two methods: `store(_:for:)` and `data(for:)`.
- **FileZone** -- Files on disk, using 2-character prefix subdirectories for filesystem efficiency. Multi-process safe.
- **SQLiteZone** -- SQLite-backed. Not thread-safe by design (the caller manages concurrency). Available via `LLVSSQLite`.
### Map internals
The 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).
Subnodes 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.
## Samples
The _Samples_ directory includes three example projects:
- **TheMessage** -- A minimal app that syncs a single shared message via CloudKit. Good for understanding the basics.
- **LoCo** -- A contact book app using UIKit.
- **LoCo-SwiftUI** -- The same contact book app built with SwiftUI.
## Learning More
There are useful posts at the [LLVS Blog](https://mentalfaculty.github.io/LLVS/).
================================================
FILE: Samples/LoCo/LoCo/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Samples/LoCo/LoCo/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Samples/LoCo/LoCo/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Samples/LoCo/LoCo/Contact.swift
================================================
//
// Contact.swift
// LoCo
//
// Created by Drew McCormack on 04/03/2026.
//
import Foundation
import LLVS
import LLVSModel
@MergeableModel
struct Contact: StorableModel, Equatable, Identifiable, Codable {
static let modelTypeIdentifier = "Contact"
var id: UUID = .init()
var firstName: String = ""
var lastName: String = ""
var streetAddress: String = ""
var postCode: String = ""
var city: String = ""
var country: String = ""
var avatarJPEGData: Data?
var fullName: String {
switch (firstName.isEmpty, lastName.isEmpty) {
case (true, true):
return ""
case (true, false):
return lastName
case (false, true):
return firstName
case (false, false):
return "\(firstName) \(lastName)"
}
}
var fullNameOrPlaceholder: String {
fullName.isEmpty ? "New Contact" : fullName
}
}
================================================
FILE: Samples/LoCo/LoCo/ContactStore.swift
================================================
//
// ContactStore.swift
// LoCo
//
// Created by Drew McCormack on 04/03/2026.
//
import Foundation
import LLVS
import LLVSModel
import LLVSCloudKit
import CloudKit
@MainActor @Observable
class ContactStore {
var contacts: [Contact] = []
private let storeCoordinator: StoreCoordinator
@ObservationIgnored nonisolated(unsafe) private var versionTask: Task<Void, Never>?
@ObservationIgnored nonisolated(unsafe) private var pollingTask: Task<Void, Never>?
init() {
LLVS.log.level = .verbose
let coordinator = try! StoreCoordinator()
let arbiter = MergeableArbiter()
arbiter.register(Contact.self)
coordinator.mergeArbiter = arbiter
let container = CKContainer(identifier: "iCloud.com.mentalfaculty.loco")
let exchange = CloudKitExchange(with: coordinator.store, storeIdentifier: "MainStore", cloudDatabaseDescription: .privateDatabaseWithCustomZone(container, zoneIdentifier: "LoCo"))
coordinator.exchange = exchange
self.storeCoordinator = coordinator
contacts = fetchContacts()
versionTask = Task { [weak self] in
guard let self else { return }
for await _ in coordinator.currentVersionUpdates {
self.contacts = self.fetchContacts()
}
}
startPolling()
}
private func fetchContacts() -> [Contact] {
(try? storeCoordinator.fetchAllModels(Contact.self)) ?? []
}
func addContact() -> Contact {
let contact = Contact()
try! storeCoordinator.save(contact, instanceIdentifier: contact.id.uuidString)
sync()
return contact
}
func updateContact(_ contact: Contact) {
try! storeCoordinator.save(contact, instanceIdentifier: contact.id.uuidString)
sync()
}
func deleteContact(_ contact: Contact) {
try! storeCoordinator.removeModel(Contact.self, instanceIdentifier: contact.id.uuidString)
sync()
}
func sync() {
Task {
try? await storeCoordinator.exchange()
storeCoordinator.merge()
}
}
private func startPolling() {
pollingTask = Task {
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(15))
try? await storeCoordinator.exchange()
storeCoordinator.merge()
}
}
}
deinit {
versionTask?.cancel()
pollingTask?.cancel()
}
}
================================================
FILE: Samples/LoCo/LoCo/ContactView.swift
================================================
//
// ContactView.swift
// LoCo
//
// Created by Drew McCormack on 04/03/2026.
//
import SwiftUI
struct ContactView: View {
@Environment(ContactStore.self) var store
let contactId: UUID
private var contact: Contact? {
store.contacts.first(where: { $0.id == contactId })
}
var body: some View {
if var contact {
Form {
Section("Name") {
TextField("First Name", text: binding(for: \.firstName, on: &contact))
TextField("Last Name", text: binding(for: \.lastName, on: &contact))
}
Section("Address") {
TextField("Street", text: binding(for: \.streetAddress, on: &contact))
TextField("Post Code", text: binding(for: \.postCode, on: &contact))
TextField("City", text: binding(for: \.city, on: &contact))
TextField("Country", text: binding(for: \.country, on: &contact))
}
}
.navigationTitle(contact.fullNameOrPlaceholder)
} else {
ContentUnavailableView("Contact Not Found", systemImage: "person.slash")
}
}
private func binding(for keyPath: WritableKeyPath<Contact, String>, on contact: inout Contact) -> Binding<String> {
Binding(
get: { self.contact?[keyPath: keyPath] ?? "" },
set: { newValue in
if var updated = self.contact {
updated[keyPath: keyPath] = newValue
store.updateContact(updated)
}
}
)
}
}
================================================
FILE: Samples/LoCo/LoCo/ContactsView.swift
================================================
//
// ContactsView.swift
// LoCo
//
// Created by Drew McCormack on 04/03/2026.
//
import SwiftUI
struct ContactsView: View {
@Environment(ContactStore.self) var store
var body: some View {
NavigationStack {
List {
ForEach(store.contacts.sorted(by: { $0.fullNameOrPlaceholder < $1.fullNameOrPlaceholder })) { contact in
NavigationLink(value: contact.id) {
VStack(alignment: .leading) {
Text(contact.fullNameOrPlaceholder)
.font(.headline)
if !contact.city.isEmpty {
Text(contact.city)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
.onDelete(perform: deleteContacts)
}
.navigationTitle("Contacts")
.navigationDestination(for: UUID.self) { contactId in
ContactView(contactId: contactId)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
_ = store.addContact()
} label: {
Image(systemName: "plus")
}
}
}
}
}
private func deleteContacts(at offsets: IndexSet) {
let sorted = store.contacts.sorted(by: { $0.fullNameOrPlaceholder < $1.fullNameOrPlaceholder })
for index in offsets {
store.deleteContact(sorted[index])
}
}
}
================================================
FILE: Samples/LoCo/LoCo/LoCo.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.mentalfaculty.loco</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>
================================================
FILE: Samples/LoCo/LoCo/LoCoApp.swift
================================================
//
// LoCoApp.swift
// LoCo
//
// Created by Drew McCormack on 04/03/2026.
//
import SwiftUI
@main
struct LoCoApp: App {
@State private var store = ContactStore()
var body: some Scene {
WindowGroup {
ContactsView()
.environment(store)
}
}
}
================================================
FILE: Samples/LoCo/LoCo/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Samples/LoCo/LoCo.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
AA0001012D97B1E200000001 /* LoCoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002022D97B1E200000001 /* LoCoApp.swift */; };
AA0001022D97B1E200000001 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002032D97B1E200000001 /* Contact.swift */; };
AA0001042D97B1E200000001 /* ContactStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002052D97B1E200000001 /* ContactStore.swift */; };
AA0001052D97B1E200000001 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002062D97B1E200000001 /* ContactsView.swift */; };
AA0001062D97B1E200000001 /* ContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002072D97B1E200000001 /* ContactView.swift */; };
AA0001072D97B1E200000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA0002082D97B1E200000001 /* Assets.xcassets */; };
AA0001082D97B1E200000001 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA0002092D97B1E200000001 /* Preview Assets.xcassets */; };
AA0001092D97B1E200000001 /* LLVS in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A012D97B1E200000001 /* LLVS */; };
AA00010A2D97B1E200000001 /* LLVSCloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A022D97B1E200000001 /* LLVSCloudKit */; };
AA00010B2D97B1E200000001 /* LLVSModel in Frameworks */ = {isa = PBXBuildFile; productRef = AA000A032D97B1E200000001 /* LLVSModel */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
AA0002012D97B1E200000001 /* LoCo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LoCo.app; sourceTree = BUILT_PRODUCTS_DIR; };
AA0002022D97B1E200000001 /* LoCoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoCoApp.swift; sourceTree = "<group>"; };
AA0002032D97B1E200000001 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
AA0002052D97B1E200000001 /* ContactStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactStore.swift; sourceTree = "<group>"; };
AA0002062D97B1E200000001 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
AA0002072D97B1E200000001 /* ContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactView.swift; sourceTree = "<group>"; };
AA0002082D97B1E200000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
AA0002092D97B1E200000001 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
AA00020A2D97B1E200000001 /* LoCo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoCo.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
AA0004012D97B1E200000001 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AA0001092D97B1E200000001 /* LLVS in Frameworks */,
AA00010A2D97B1E200000001 /* LLVSCloudKit in Frameworks */,
AA00010B2D97B1E200000001 /* LLVSModel in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
AA0003012D97B1E200000001 = {
isa = PBXGroup;
children = (
AA0003032D97B1E200000001 /* LoCo */,
AA0003022D97B1E200000001 /* Products */,
);
sourceTree = "<group>";
};
AA0003022D97B1E200000001 /* Products */ = {
isa = PBXGroup;
children = (
AA0002012D97B1E200000001 /* LoCo.app */,
);
name = Products;
sourceTree = "<group>";
};
AA0003032D97B1E200000001 /* LoCo */ = {
isa = PBXGroup;
children = (
AA00020A2D97B1E200000001 /* LoCo.entitlements */,
AA0002022D97B1E200000001 /* LoCoApp.swift */,
AA0002032D97B1E200000001 /* Contact.swift */,
AA0002052D97B1E200000001 /* ContactStore.swift */,
AA0002062D97B1E200000001 /* ContactsView.swift */,
AA0002072D97B1E200000001 /* ContactView.swift */,
AA0002082D97B1E200000001 /* Assets.xcassets */,
AA0003042D97B1E200000001 /* Preview Content */,
);
path = LoCo;
sourceTree = "<group>";
};
AA0003042D97B1E200000001 /* Preview Content */ = {
isa = PBXGroup;
children = (
AA0002092D97B1E200000001 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
AA0005012D97B1E200000001 /* LoCo */ = {
isa = PBXNativeTarget;
buildConfigurationList = AA0008022D97B1E200000001 /* Build configuration list for PBXNativeTarget "LoCo" */;
buildPhases = (
AA0004032D97B1E200000001 /* Sources */,
AA0004012D97B1E200000001 /* Frameworks */,
AA0004022D97B1E200000001 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = LoCo;
packageProductDependencies = (
AA000A012D97B1E200000001 /* LLVS */,
AA000A022D97B1E200000001 /* LLVSCloudKit */,
AA000A032D97B1E200000001 /* LLVSModel */,
);
productName = LoCo;
productReference = AA0002012D97B1E200000001 /* LoCo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
AA0006012D97B1E200000001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 2630;
ORGANIZATIONNAME = "Momenta B.V.";
TargetAttributes = {
AA0005012D97B1E200000001 = {
CreatedOnToolsVersion = 16.0;
};
};
};
buildConfigurationList = AA0008012D97B1E200000001 /* Build configuration list for PBXProject "LoCo" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = AA0003012D97B1E200000001;
packageReferences = (
AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference "../.." */,
);
productRefGroup = AA0003022D97B1E200000001 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
AA0005012D97B1E200000001 /* LoCo */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
AA0004022D97B1E200000001 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AA0001082D97B1E200000001 /* Preview Assets.xcassets in Resources */,
AA0001072D97B1E200000001 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
AA0004032D97B1E200000001 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AA0001012D97B1E200000001 /* LoCoApp.swift in Sources */,
AA0001022D97B1E200000001 /* Contact.swift in Sources */,
AA0001042D97B1E200000001 /* ContactStore.swift in Sources */,
AA0001052D97B1E200000001 /* ContactsView.swift in Sources */,
AA0001062D97B1E200000001 /* ContactView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
AA0007012D97B1E200000001 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
AA0007022D97B1E200000001 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
AA0007032D97B1E200000001 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = LoCo/LoCo.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"LoCo/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.loco;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
AA0007042D97B1E200000001 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = LoCo/LoCo.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"LoCo/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.loco;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
AA0008012D97B1E200000001 /* Build configuration list for PBXProject "LoCo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AA0007012D97B1E200000001 /* Debug */,
AA0007022D97B1E200000001 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
AA0008022D97B1E200000001 /* Build configuration list for PBXNativeTarget "LoCo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AA0007032D97B1E200000001 /* Debug */,
AA0007042D97B1E200000001 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference "../.." */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../..;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
AA000A012D97B1E200000001 /* LLVS */ = {
isa = XCSwiftPackageProductDependency;
package = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference "../.." */;
productName = LLVS;
};
AA000A022D97B1E200000001 /* LLVSCloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference "../.." */;
productName = LLVSCloudKit;
};
AA000A032D97B1E200000001 /* LLVSModel */ = {
isa = XCSwiftPackageProductDependency;
package = AA0009012D97B1E200000001 /* XCLocalSwiftPackageReference "../.." */;
productName = LLVSModel;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = AA0006012D97B1E200000001 /* Project object */;
}
================================================
FILE: Samples/TheMessage/TheMessage/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Samples/TheMessage/TheMessage/Assets.xcassets/Contents.json
================================================
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Samples/TheMessage/TheMessage/ContentView.swift
================================================
import SwiftUI
struct ContentView: View {
@Environment(MessageStore.self) var store
@State private var draft: String = ""
var body: some View {
VStack(spacing: 20) {
Text(store.message)
.font(.title2)
.multilineTextAlignment(.center)
.padding()
HStack {
TextField("New message", text: $draft)
.textFieldStyle(.roundedBorder)
Button("Post") {
guard !draft.isEmpty else { return }
store.post(message: draft)
draft = ""
}
.buttonStyle(.borderedProminent)
}
.padding(.horizontal)
}
}
}
================================================
FILE: Samples/TheMessage/TheMessage/MessageStore.swift
================================================
import Foundation
import LLVS
import LLVSCloudKit
import CloudKit
@MainActor @Observable
class MessageStore {
var message: String = "Let there be light!"
private let storeCoordinator: StoreCoordinator
private let messageId = Value.ID("MESSAGE")
@ObservationIgnored nonisolated(unsafe) private var versionTask: Task<Void, Never>?
@ObservationIgnored nonisolated(unsafe) private var pollingTask: Task<Void, Never>?
init() {
LLVS.log.level = .verbose
let coordinator = try! StoreCoordinator()
let container = CKContainer(identifier: "iCloud.com.mentalfaculty.themessage")
let exchange = CloudKitExchange(with: coordinator.store, storeIdentifier: "MainStore", cloudDatabaseDescription: .publicDatabase(container))
coordinator.exchange = exchange
self.storeCoordinator = coordinator
versionTask = Task { [weak self] in
guard let self else { return }
for await _ in coordinator.currentVersionUpdates {
self.message = self.fetchMessage() ?? "Let there be light!"
}
}
startPolling()
}
private func fetchMessage() -> String? {
guard let value = try? storeCoordinator.value(id: messageId) else { return nil }
return String(data: value.data, encoding: .utf8)
}
func post(message: String) {
let data = message.data(using: .utf8)!
let newValue = Value(id: messageId, data: data)
try! storeCoordinator.save(updating: [newValue])
sync()
}
func sync() {
Task {
try? await storeCoordinator.exchange()
storeCoordinator.merge()
}
}
private func startPolling() {
pollingTask = Task {
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(15))
try? await storeCoordinator.exchange()
storeCoordinator.merge()
}
}
}
deinit {
versionTask?.cancel()
pollingTask?.cancel()
}
}
================================================
FILE: Samples/TheMessage/TheMessage/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Samples/TheMessage/TheMessage/TheMessage.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.mentalfaculty.themessage</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>
================================================
FILE: Samples/TheMessage/TheMessage/TheMessageApp.swift
================================================
import SwiftUI
@main
struct TheMessageApp: App {
@State private var store = MessageStore()
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}
================================================
FILE: Samples/TheMessage/TheMessage.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
07E880462350EDE5003471B4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880452350EDE5003471B4 /* ContentView.swift */; };
07E880482350EDE6003471B4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07E880472350EDE6003471B4 /* Assets.xcassets */; };
07E8804B2350EDE6003471B4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */; };
07E8805E2350EF64003471B4 /* LLVS in Frameworks */ = {isa = PBXBuildFile; productRef = 07E8805D2350EF64003471B4 /* LLVS */; };
07E880602350EF64003471B4 /* LLVSCloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 07E8805F2350EF64003471B4 /* LLVSCloudKit */; };
07E880632350EF64003471B4 /* TheMessageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880612350EF64003471B4 /* TheMessageApp.swift */; };
07E880642350EF64003471B4 /* MessageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E880622350EF64003471B4 /* MessageStore.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
07E8803E2350EDE5003471B4 /* TheMessage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TheMessage.app; sourceTree = BUILT_PRODUCTS_DIR; };
07E880452350EDE5003471B4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
07E880472350EDE6003471B4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
07E880552350EE21003471B4 /* TheMessage.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TheMessage.entitlements; sourceTree = "<group>"; };
07E880612350EF64003471B4 /* TheMessageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheMessageApp.swift; sourceTree = "<group>"; };
07E880622350EF64003471B4 /* MessageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStore.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
07E8803B2350EDE5003471B4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
07E8805E2350EF64003471B4 /* LLVS in Frameworks */,
07E880602350EF64003471B4 /* LLVSCloudKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
07E880352350EDE5003471B4 = {
isa = PBXGroup;
children = (
07E880402350EDE5003471B4 /* TheMessage */,
07E8803F2350EDE5003471B4 /* Products */,
);
sourceTree = "<group>";
};
07E8803F2350EDE5003471B4 /* Products */ = {
isa = PBXGroup;
children = (
07E8803E2350EDE5003471B4 /* TheMessage.app */,
);
name = Products;
sourceTree = "<group>";
};
07E880402350EDE5003471B4 /* TheMessage */ = {
isa = PBXGroup;
children = (
07E880552350EE21003471B4 /* TheMessage.entitlements */,
07E880612350EF64003471B4 /* TheMessageApp.swift */,
07E880622350EF64003471B4 /* MessageStore.swift */,
07E880452350EDE5003471B4 /* ContentView.swift */,
07E880472350EDE6003471B4 /* Assets.xcassets */,
07E880492350EDE6003471B4 /* Preview Content */,
);
path = TheMessage;
sourceTree = "<group>";
};
07E880492350EDE6003471B4 /* Preview Content */ = {
isa = PBXGroup;
children = (
07E8804A2350EDE6003471B4 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
07E8803D2350EDE5003471B4 /* TheMessage */ = {
isa = PBXNativeTarget;
buildConfigurationList = 07E880522350EDE6003471B4 /* Build configuration list for PBXNativeTarget "TheMessage" */;
buildPhases = (
07E8803A2350EDE5003471B4 /* Sources */,
07E8803B2350EDE5003471B4 /* Frameworks */,
07E8803C2350EDE5003471B4 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TheMessage;
packageProductDependencies = (
07E8805D2350EF64003471B4 /* LLVS */,
07E8805F2350EF64003471B4 /* LLVSCloudKit */,
);
productName = TheMessage;
productReference = 07E8803E2350EDE5003471B4 /* TheMessage.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
07E880362350EDE5003471B4 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 2630;
ORGANIZATIONNAME = "Momenta B.V.";
TargetAttributes = {
07E8803D2350EDE5003471B4 = {
CreatedOnToolsVersion = 11.1;
};
};
};
buildConfigurationList = 07E880392350EDE5003471B4 /* Build configuration list for PBXProject "TheMessage" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 07E880352350EDE5003471B4;
packageReferences = (
07E880652350EF64003471B4 /* XCLocalSwiftPackageReference "../.." */,
);
productRefGroup = 07E8803F2350EDE5003471B4 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
07E8803D2350EDE5003471B4 /* TheMessage */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
07E8803C2350EDE5003471B4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07E8804B2350EDE6003471B4 /* Preview Assets.xcassets in Resources */,
07E880482350EDE6003471B4 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
07E8803A2350EDE5003471B4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07E880632350EF64003471B4 /* TheMessageApp.swift in Sources */,
07E880642350EF64003471B4 /* MessageStore.swift in Sources */,
07E880462350EDE5003471B4 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
07E880502350EDE6003471B4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
07E880512350EDE6003471B4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
07E880532350EDE6003471B4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TheMessage/TheMessage.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"TheMessage/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.themessage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
07E880542350EDE6003471B4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TheMessage/TheMessage.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"TheMessage/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mentalfaculty.themessage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
07E880392350EDE5003471B4 /* Build configuration list for PBXProject "TheMessage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
07E880502350EDE6003471B4 /* Debug */,
07E880512350EDE6003471B4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
07E880522350EDE6003471B4 /* Build configuration list for PBXNativeTarget "TheMessage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
07E880532350EDE6003471B4 /* Debug */,
07E880542350EDE6003471B4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
07E880652350EF64003471B4 /* XCLocalSwiftPackageReference "../.." */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../..;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
07E8805D2350EF64003471B4 /* LLVS */ = {
isa = XCSwiftPackageProductDependency;
package = 07E880652350EF64003471B4 /* XCLocalSwiftPackageReference "../.." */;
productName = LLVS;
};
07E8805F2350EF64003471B4 /* LLVSCloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 07E880652350EF64003471B4 /* XCLocalSwiftPackageReference "../.." */;
productName = LLVSCloudKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 07E880362350EDE5003471B4 /* Project object */;
}
================================================
FILE: Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:TheMessage.xcodeproj">
</FileRef>
</Workspace>
================================================
FILE: Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: Sources/LLVS/Core/Exchange.swift
================================================
//
// Exchange.swift
// LLVS
//
// Created by Drew McCormack on 25/02/2019.
//
import Foundation
enum ExchangeError: Swift.Error {
case remoteVersionsWithUnknownPredecessors
case missingVersion
case unknown(error: Swift.Error)
}
public typealias VersionChanges = (version: Version, valueChanges: [Value.Change])
public protocol SnapshotExchange {
func retrieveSnapshotManifest() async throws -> SnapshotManifest?
func retrieveSnapshotChunk(index: Int) async throws -> Data
func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws
}
public protocol Exchange: AnyObject {
var newVersionsAvailable: AsyncStream<Void> { get }
var store: Store { get }
var restorationState: Data? { get set }
func retrieve() async throws -> [Version.ID]
func prepareToRetrieve() async throws
func retrieveAllVersionIdentifiers() async throws -> [Version.ID]
func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version]
func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]]
func send() async throws -> [Version.ID]
func prepareToSend() async throws
func send(versionChanges: [VersionChanges]) async throws
}
// MARK:- Retrieving
public extension Exchange {
func retrieve() async throws -> [Version.ID] {
log.trace("Retrieving")
try await self.prepareToRetrieve()
let remoteIds = try await self.retrieveAllVersionIdentifiers()
let toRetrieveIds = self.versionIdsMissingFromHistory(forRemoteIdentifiers: remoteIds)
log.verbose("Version identifiers to retrieve: \(toRetrieveIds.idStrings)")
let remoteVersions = try await self.retrieveVersions(identifiedBy: toRetrieveIds)
log.verbose("Adding to history versions: \(remoteVersions.idStrings)")
try await self.addToHistory(remoteVersions)
log.trace("Retrieved")
return remoteIds
}
private func versionIdsMissingFromHistory(forRemoteIdentifiers remoteIdentifiers: [Version.ID]) -> [Version.ID] {
var toRetrieveIds: [Version.ID]!
self.store.queryHistory { history in
let storeVersionIds = Set(history.allVersionIdentifiers)
let remoteVersionIds = Set(remoteIdentifiers)
toRetrieveIds = Array(remoteVersionIds.subtracting(storeVersionIds))
}
return toRetrieveIds
}
private func addToHistory(_ versions: [Version]) async throws {
let versionsByIdentifier = versions.reduce(into: [:]) { result, version in
result[version.id] = version
}
let sortedVersions = versions.sorted { $0.timestamp < $1.timestamp }
func batchSizeCostEvaluator(index: Int) -> Float {
let batchDataSizeLimit = 5000000 // 5MB
let version = sortedVersions[index]
return Float(version.valueDataSize ?? 100000) / Float(batchDataSizeLimit)
}
let dynamicBatcher = DynamicTaskBatcher(numberOfTasks: sortedVersions.count, taskCostEvaluator: batchSizeCostEvaluator) { range in
let batchVersions = Array(sortedVersions[range])
let valueChangesByVersionIdentifier: [Version.ID: [Value.Change]]
do {
valueChangesByVersionIdentifier = try await self.retrieveValueChanges(forVersionsIdentifiedBy: batchVersions.ids)
} catch {
log.error("Failed adding to history: \(error)")
return .definitive(.failure(error))
}
let valueChangesByVersionID: [Version.ID: [Value.Change]] = valueChangesByVersionIdentifier.reduce(into: [:]) { result, keyValue in
var version = versionsByIdentifier[keyValue.key]!
if version.valueDataSize == nil { version.valueDataSize = keyValue.value.valueDataSize }
result[version.id] = keyValue.value
}
do {
try self.addToHistorySync(sortedVersions: batchVersions, valueChangesByVersionID: valueChangesByVersionID)
return .definitive(.success(()))
} catch let error as ExchangeError where error.isUnknownPredecessors {
return .growBatchAndReexecute
} catch {
return .definitive(.failure(error))
}
}
try await dynamicBatcher.start()
}
/// Synchronously add sorted versions to history, iterating until all appendable versions are consumed.
private func addToHistorySync(sortedVersions: [Version], valueChangesByVersionID: [Version.ID: [Value.Change]]) throws {
var remaining = sortedVersions
while !remaining.isEmpty {
guard let version = appendableVersion(from: remaining) else {
log.error("Failed to add to history due to missing predecessors")
throw ExchangeError.remoteVersionsWithUnknownPredecessors
}
let valueChanges = valueChangesByVersionID[version.id]!
log.trace("Adding version to store: \(version.id.rawValue)")
log.verbose("Value changes for \(version.id.rawValue): \(valueChanges)")
do {
try self.store.addVersion(version, storing: valueChanges)
} catch Store.Error.attemptToAddExistingVersion {
log.error("Failed adding to history because version already exists. Ignoring error")
}
remaining = remaining.filter { $0.id != version.id }
}
}
private func appendableVersion(from versions: [Version]) -> Version? {
return versions.first { v in
return store.historyIncludesVersions(identifiedBy: v.predecessors?.ids ?? [])
}
}
}
// MARK:- Sending
public extension Exchange {
func send() async throws -> [Version.ID] {
try await self.prepareToSend()
let remoteIds = try await self.retrieveAllVersionIdentifiers()
let toSendIds = self.versionIdsMissingRemotely(forRemoteIdentifiers: remoteIds)
func batchSizeCostEvaluator(index: Int) -> Float {
let batchDataSizeLimit: Int64 = 5000000 // 5MB
let defaultDataSize: Int64 = 100000 // 100KB
if let version = try? self.store.version(identifiedBy: toSendIds[index]) {
return Float(version.valueDataSize ?? defaultDataSize) / Float(batchDataSizeLimit)
} else {
return Float(defaultDataSize) / Float(batchDataSizeLimit)
}
}
let taskBatcher = DynamicTaskBatcher(numberOfTasks: toSendIds.count, taskCostEvaluator: batchSizeCostEvaluator) { range in
do {
let versionChanges: [VersionChanges] = try toSendIds[range].map { versionId in
guard let version = try self.store.version(identifiedBy: versionId) else {
throw ExchangeError.missingVersion
}
let changes = try self.store.valueChanges(madeInVersionIdentifiedBy: versionId)
return (version, changes)
}
guard !versionChanges.isEmpty else {
return .definitive(.success(()))
}
try await self.send(versionChanges: versionChanges)
return .definitive(.success(()))
} catch {
return .definitive(.failure(error))
}
}
try await taskBatcher.start()
return toSendIds
}
private func versionIdsMissingRemotely(forRemoteIdentifiers remoteIdentifiers: [Version.ID]) -> [Version.ID] {
var toSendIds: [Version.ID]!
self.store.queryHistory { history in
let storeVersionIds = Set(history.allVersionIdentifiers)
let remoteVersionIds = Set(remoteIdentifiers)
toSendIds = Array(storeVersionIds.subtracting(remoteVersionIds))
}
return toSendIds
}
}
// MARK:- ExchangeError helper
fileprivate extension ExchangeError {
var isUnknownPredecessors: Bool {
if case .remoteVersionsWithUnknownPredecessors = self { return true }
return false
}
}
================================================
FILE: Sources/LLVS/Core/FileZone.swift
================================================
//
// FileZone.swift
// LLVS
//
// Created by Drew McCormack on 14/05/2019.
//
import Foundation
public class FileStorage: Storage, SnapshotCapable {
private let fileExtension = "json"
public init() {}
public func makeMapZone(for type: MapType, in store: Store) -> Zone {
switch type {
case .valuesByVersion:
return FileZone(rootDirectory: store.valuesMapDirectoryURL, fileExtension: fileExtension)
case .userDefined:
fatalError("User defined maps not yet supported")
}
}
public func makeValuesZone(in store: Store) -> Zone {
return FileZone(rootDirectory: store.valuesDirectoryURL, fileExtension: fileExtension)
}
}
internal final class FileZone: Zone {
let rootDirectory: URL
let fileExtension: String
private let uncachableDataSizeLimit = 10000 // 10KB
private let cache: Cache<Data> = .init()
fileprivate let fileManager = FileManager()
init(rootDirectory: URL, fileExtension: String) {
self.rootDirectory = rootDirectory.resolvingSymlinksInPath()
try? fileManager.createDirectory(at: rootDirectory, withIntermediateDirectories: true, attributes: nil)
self.fileExtension = fileExtension
}
private func cacheIfNeeded(_ data: Data, for reference: ZoneReference) {
if data.count < uncachableDataSizeLimit {
cache.setValue(data, for: reference)
}
}
internal func store(_ data: Data, for reference: ZoneReference) throws {
let (dir, file) = try fileSystemLocation(for: reference)
try? fileManager.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
let compressed = DataCompression.compress(data)
try compressed.write(to: file)
cacheIfNeeded(data, for: reference)
}
internal func data(for reference: ZoneReference) throws -> Data? {
if let data = cache.value(for: reference) { return data }
let (_, file) = try fileSystemLocation(for: reference)
guard let raw = try? Data(contentsOf: file) else { return nil }
let data = DataCompression.decompressIfNeeded(raw)
cacheIfNeeded(data, for: reference)
return data
}
func fileSystemLocation(for reference: ZoneReference) throws -> (directoryURL: URL, fileURL: URL) {
let safeKey = reference.key.replacingOccurrences(of: "/", with: "LLVSSLASH").replacingOccurrences(of: ":", with: "LLVSCOLON")
let valueDirectoryURL = rootDirectory.appendingSplitPathComponent(safeKey)
let versionName = reference.version.rawValue + "." + fileExtension
let fileURL = valueDirectoryURL.appendingSplitPathComponent(versionName, prefixLength: 1)
let directoryURL = fileURL.deletingLastPathComponent()
return (directoryURL: directoryURL, fileURL: fileURL)
}
internal func versionIds(for key: String) throws -> [Version.ID] {
let valueDirectoryURL = rootDirectory.appendingSplitPathComponent(key)
let valueDirLength = valueDirectoryURL.path.count
let enumerator = fileManager.enumerator(at: valueDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!
var versions: [Version.ID] = []
let slash = Character("/")
for any in enumerator {
var isDirectory: ObjCBool = true
guard let url = any as? URL else { continue }
guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }
let path = url.resolvingSymlinksInPath().deletingPathExtension().path
let index = path.index(path.startIndex, offsetBy: Int(valueDirLength))
let version = String(path[index...]).filter { $0 != slash }
versions.append(Version.ID(version))
}
return versions
}
}
================================================
FILE: Sources/LLVS/Core/FolderBasedExchange.swift
================================================
//
// FolderBasedExchange.swift
// LLVS
//
// Created by Drew McCormack on 01/03/2026.
//
import Foundation
public enum FolderBasedExchangeError: Error {
case versionFileInvalid
case fileNotFound(String)
case foldersNotInitialized
}
public protocol FolderBasedExchange: Exchange {
associatedtype FileID
associatedtype FolderID
var versionsFolderID: FolderID? { get }
var changesFolderID: FolderID? { get }
func notifyNewVersionsAvailable()
func listFiles(inFolder folder: FolderID) async throws -> [String: FileID]
func downloadData(forFile fileID: FileID) async throws -> Data
func uploadData(_ data: Data, named name: String, toFolder folder: FolderID) async throws
}
public extension FolderBasedExchange {
func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {
guard let folderID = versionsFolderID else { return [] }
let fileMap = try await listFiles(inFolder: folderID)
return fileMap.map { Version.ID($0.key) }
}
func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {
guard !versionIds.isEmpty else { return [] }
guard let folderID = versionsFolderID else { return [] }
let fileMap = try await listFiles(inFolder: folderID)
var versions: [Version] = []
for versionId in versionIds {
guard let fileID = fileMap[versionId.rawValue] else {
throw FolderBasedExchangeError.fileNotFound(versionId.rawValue)
}
let data = try await downloadData(forFile: fileID)
if let version = try JSONDecoder().decode([String: Version].self, from: data)["version"] {
versions.append(version)
} else {
throw FolderBasedExchangeError.versionFileInvalid
}
}
return versions
}
func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {
guard !versionIds.isEmpty else { return [:] }
guard let folderID = changesFolderID else { return [:] }
let fileMap = try await listFiles(inFolder: folderID)
var changesByVersion: [Version.ID: [Value.Change]] = [:]
for versionId in versionIds {
guard let fileID = fileMap[versionId.rawValue] else {
throw FolderBasedExchangeError.fileNotFound(versionId.rawValue)
}
let data = try await downloadData(forFile: fileID)
let changes = try JSONDecoder().decode([Value.Change].self, from: data)
changesByVersion[versionId] = changes
}
return changesByVersion
}
func send(versionChanges: [VersionChanges]) async throws {
guard !versionChanges.isEmpty else { return }
guard let versionsFolderID = versionsFolderID,
let changesFolderID = changesFolderID else {
throw FolderBasedExchangeError.foldersNotInitialized
}
for (version, valueChanges) in versionChanges {
let changesData = try JSONEncoder().encode(valueChanges)
let versionData = try JSONEncoder().encode(["version": version])
try await uploadData(changesData, named: version.id.rawValue, toFolder: changesFolderID)
try await uploadData(versionData, named: version.id.rawValue, toFolder: versionsFolderID)
}
notifyNewVersionsAvailable()
}
}
================================================
FILE: Sources/LLVS/Core/History.swift
================================================
//
// History.swift
// LLVS
//
// Created by Drew McCormack on 11/11/2018.
//
import Foundation
public class History {
public enum Error: Swift.Error {
case attemptToAddPreexistingVersion(id: String)
case nonExistentVersionEncountered(identifier: String)
}
private var versionsByIdentifier: [Version.ID:Version] = [:]
private var referencedVersionIdentifiers: Set<Version.ID> = [] // Any version that is a predecessor
public private(set) var headIdentifiers: Set<Version.ID> = [] // Versions that are not predecessors of other versions
public var allVersionIdentifiers: [Version.ID] { return Array(versionsByIdentifier.keys) }
public var mostRecentHead: Version? {
let maxId = headIdentifiers.max { (vId1, vId2) -> Bool in
let v1 = version(identifiedBy: vId1)!
let v2 = version(identifiedBy: vId2)!
return v1.timestamp < v2.timestamp
}
return maxId.flatMap { version(identifiedBy: $0) }
}
public func version(identifiedBy identifier: Version.ID) -> Version? {
return versionsByIdentifier[identifier]
}
internal func version(prevailingFromCandidates candidates: [Version.ID], at versionId: Version.ID) -> Version? {
if let candidate = candidates.first(where: { $0 == versionId }) {
return version(identifiedBy: candidate)
}
var ancestors: Set<Version.ID> = [versionId]
for v in self {
// See if v is in our ancestry. If so, extend ancestry.
if ancestors.contains(v.id) {
ancestors.formUnion(v.predecessors?.ids ?? [])
ancestors.remove(v.id)
}
if let candidate = candidates.first(where: { ancestors.contains($0) }) {
return version(identifiedBy: candidate)
}
}
return nil
}
internal func isAncestralLine(from ancestor: Version.ID, to descendant: Version.ID) -> Bool {
return nil != version(prevailingFromCandidates: [ancestor], at: descendant)
}
/// If updatingPredecessorVersions is true, the successors of other versions may be updated.
/// Use this when adding a new head when storing.
/// Pass in false if more control is needed over setting the successors, such as
/// when loading them to setup the History. In that case, we only want to set them when all versions
/// have been loaded.
internal func add(_ version: Version, updatingPredecessorVersions: Bool) throws {
guard versionsByIdentifier[version.id] == nil else {
throw Error.attemptToAddPreexistingVersion(id: version.id.rawValue)
}
versionsByIdentifier[version.id] = version
if updatingPredecessorVersions {
try updateSuccessors(inPredecessorsOf: version)
}
if !referencedVersionIdentifiers.contains(version.id) {
headIdentifiers.insert(version.id)
}
}
internal func updateSuccessors(inPredecessorsOf version: Version) throws {
for predecessorIdentifier in version.predecessors?.ids ?? [] {
guard let predecessor = self.version(identifiedBy: predecessorIdentifier) else {
throw Error.nonExistentVersionEncountered(identifier: predecessorIdentifier.rawValue)
}
referencedVersionIdentifiers.insert(predecessorIdentifier)
headIdentifiers.remove(predecessorIdentifier)
var newPredecessor = predecessor
let newSuccessorIdentifiers = predecessor.successors.ids.union([version.id])
newPredecessor.successors = Version.Successors(ids: newSuccessorIdentifiers)
versionsByIdentifier[newPredecessor.id] = newPredecessor
}
}
/// Finds the greatest common ancestor of all the given version IDs by pairwise reduction.
internal func greatestCommonAncestor(ofAll versionIds: Set<Version.ID>) throws -> Version.ID? {
guard !versionIds.isEmpty else { return nil }
var ids = Array(versionIds)
var result = ids.removeFirst()
for id in ids {
guard let gca = try greatestCommonAncestor(ofVersionsIdentifiedBy: (result, id)) else {
return nil
}
result = gca
}
return result
}
public func greatestCommonAncestor(ofVersionsIdentifiedBy ids: (Version.ID, Version.ID)) throws -> Version.ID? {
// Find all ancestors of first Version. Determine how many generations back each Version is.
// We take the shortest path to any given Version, ie, the minimum of possible paths.
var generationById = [Version.ID:Int]()
var firstFront: Set<Version.ID> = [ids.0]
func propagateFront(front: inout Set<Version.ID>) throws {
var newFront = Set<Version.ID>()
for identifier in front {
guard let frontVersion = self.version(identifiedBy: identifier) else {
throw Error.nonExistentVersionEncountered(identifier: identifier.rawValue)
}
newFront.formUnion(frontVersion.predecessors?.ids ?? [])
}
front = newFront
}
var generation = 0
while firstFront.count > 0 {
firstFront.forEach { generationById[$0] = Swift.min(generationById[$0] ?? Int.max, generation) }
try propagateFront(front: &firstFront)
generation += 1
}
// Now go through ancestors of second version until we find the first in common with the first ancestors
var secondFront: Set<Version.ID> = [ids.1]
let ancestorsOfFirst = Set(generationById.keys)
while secondFront.count > 0 {
let common = ancestorsOfFirst.intersection(secondFront)
let sorted = common.sorted { generationById[$0]! < generationById[$1]! }
if let mostRecentCommon = sorted.first { return mostRecentCommon }
try propagateFront(front: &secondFront)
}
return nil
}
}
extension History: Sequence {
/// Enumerates history in a topological sorted order.
/// Note that there are many possible orders that satisfy this.
/// Most recent versions are ordered first (ie heads).
/// Return false from block to stop.
/// Uses Kahn algorithm to generate the order. https://en.wikipedia.org/wiki/Topological_sorting
public struct TopologicalIterator: IteratorProtocol {
public typealias Element = Version
public let history: History
private var front: Set<Version>
private var referenceCountByIdentifier: [Version.ID:Int] = [:]
init(toIterate history: History) {
self.history = history
let headVersions = history.headIdentifiers.map {
history.version(identifiedBy: $0)!
}
self.front = Set(headVersions)
}
public mutating func next() -> Version? {
guard let next = front.first(where: { version in
let refCount = self.referenceCountByIdentifier[version.id] ?? 0
let successorCount = version.successors.ids.count
return refCount == successorCount
})
else {
return nil
}
for predecessorIdentifier in next.predecessors?.ids ?? [] {
let predecessor = history.version(identifiedBy: predecessorIdentifier)!
referenceCountByIdentifier[predecessor.id] = (referenceCountByIdentifier[predecessor.id] ?? 0) + 1
front.insert(predecessor)
}
front.remove(next)
return next
}
}
public func makeIterator() -> History.TopologicalIterator {
return Iterator(toIterate: self)
}
}
================================================
FILE: Sources/LLVS/Core/Map.swift
================================================
//
// Map.swift
// LLVS
//
// Created by Drew McCormack on 30/11/2018.
//
import Foundation
final class Map {
enum Error: Swift.Error {
case encodingFailure(String)
case unexpectedNodeContent
case missingNode
case missingVersionRoot
}
let zone: Zone
private let nodeCache: Cache<Node> = .init()
private let rootKey = "__llvs_root"
init(zone: Zone) {
self.zone = zone
}
func addVersion(_ version: Version.ID, basedOn baseVersion: Version.ID?, applying deltas: [Delta]) throws {
try autoreleasepool {
let encoder = JSONEncoder()
var rootNode: Node
let rootRef = ZoneReference(key: rootKey, version: version)
if let baseVersion = baseVersion {
let oldRootRef = ZoneReference(key: rootKey, version: baseVersion)
guard let oldRoot = try node(for: oldRootRef) else { throw Error.missingNode }
rootNode = oldRoot
}
else {
rootNode = Node(reference: rootRef, children: .nodes([]))
}
rootNode.reference.version = version
guard case let .nodes(rootChildRefs) = rootNode.children else { throw Error.unexpectedNodeContent }
var subNodesByKey: [Key:Node] = [:]
for delta in deltas {
let key = delta.key
let subNodeKey = Key(String(key.keyString.prefix(2)))
let subNodeRef = ZoneReference(key: subNodeKey.keyString, version: version)
var subNode: Node
if let n = subNodesByKey[subNodeKey] {
subNode = n
}
else if let existingSubNodeRef = rootChildRefs.first(where: { $0.key == subNodeKey.keyString }) {
guard let existingSubNode = try node(for: existingSubNodeRef) else { throw Error.missingNode }
subNode = existingSubNode
subNode.reference = subNodeRef
}
else {
subNode = Node(reference: subNodeRef, children: .values([]))
}
guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }
let valueRefs = keyValuePairs.filter({ $0.key == key }).map({ $0.valueReference })
var valueRefsByIdentifier: [Value.ID:Value.Reference] = Dictionary(uniqueKeysWithValues: valueRefs.map({ ($0.valueId, $0) }) )
for valueRef in delta.addedValueReferences {
valueRefsByIdentifier[valueRef.valueId] = valueRef
}
for valueId in delta.removedValueIdentifiers {
valueRefsByIdentifier[valueId] = nil
}
let newValueRefs = Array(valueRefsByIdentifier.values)
var newPairs = keyValuePairs.filter { $0.key != key }
newPairs += newValueRefs.map { KeyValuePair(key: key, valueReference: $0) }
subNode.children = .values(newPairs)
subNodesByKey[subNodeKey] = subNode
}
// Update and save subnodes and rootnode
var rootRefsByIdentifier: [String:ZoneReference] = Dictionary(uniqueKeysWithValues: rootChildRefs.map({ ($0.key, $0) }) )
for subNode in subNodesByKey.values {
let key = subNode.reference.key
let data = try encoder.encode(subNode)
try zone.store(data, for: subNode.reference)
rootRefsByIdentifier[key] = subNode.reference
}
rootNode.children = .nodes(Array(rootRefsByIdentifier.values))
let data = try encoder.encode(rootNode)
try zone.store(data, for: rootNode.reference)
}
}
func purgeCache() {
nodeCache.purgeAllValues()
}
/// Returns zone references for the root node and all subnodes for a given version.
/// Used during compaction cleanup to know what Map data to delete.
func zoneReferences(forVersionIdentifiedBy versionId: Version.ID) throws -> [ZoneReference] {
let rootRef = ZoneReference(key: rootKey, version: versionId)
guard let rootNode = try node(for: rootRef) else { return [] }
var refs: [ZoneReference] = [rootRef]
if case let .nodes(subNodeRefs) = rootNode.children {
refs.append(contentsOf: subNodeRefs)
}
return refs
}
func differences(between firstVersion: Version.ID, and secondVersion: Version.ID, withCommonAncestor commonAncestor: Version.ID?) throws -> [Diff] {
let originRef = commonAncestor.flatMap { ZoneReference(key: rootKey, version: $0) }
let rootRef1 = ZoneReference(key: rootKey, version: firstVersion)
let rootRef2 = ZoneReference(key: rootKey, version: secondVersion)
let originNode = try originRef.flatMap { try node(for: $0) }
guard
let rootNode1 = try node(for: rootRef1),
let rootNode2 = try node(for: rootRef2) else {
throw Error.missingVersionRoot
}
let nodesOrigin: [ZoneReference]?
if case let .nodes(n)? = originNode?.children {
nodesOrigin = n
} else {
nodesOrigin = nil
}
guard
case let .nodes(subNodes1) = rootNode1.children,
case let .nodes(subNodes2) = rootNode2.children else {
throw Error.unexpectedNodeContent
}
let refOriginByKey: [String:ZoneReference]? = nodesOrigin.flatMap { refs in .init(uniqueKeysWithValues: refs.map({ ($0.key, $0) })) }
let subNodeRefs1ByKey: [String:ZoneReference] = .init(uniqueKeysWithValues: subNodes1.map({ ($0.key, $0) }))
let subNodeRefs2ByKey: [String:ZoneReference] = .init(uniqueKeysWithValues: subNodes2.map({ ($0.key, $0) }))
var allSubNodeKeys = Set(subNodeRefs1ByKey.keys).union(subNodeRefs2ByKey.keys)
if let r = refOriginByKey { allSubNodeKeys.formUnion(r.keys) }
// Batch prefetch only subnodes that will actually be compared
var refsToFetch: [ZoneReference] = []
for subNodeKey in allSubNodeKeys {
let r1 = subNodeRefs1ByKey[subNodeKey]
let r2 = subNodeRefs2ByKey[subNodeKey]
if r1 == r2 { continue }
if let r = r1 { refsToFetch.append(r) }
if let r = r2 { refsToFetch.append(r) }
if let r = refOriginByKey?[subNodeKey] { refsToFetch.append(r) }
}
try prefetchNodes(for: refsToFetch)
var diffs: [Diff] = []
for subNodeKey in allSubNodeKeys {
func appendDiffs(forIdentifiers ids: [Value.ID], fork: Value.Fork) throws {
for id in ids {
let diff = Diff(key: .init(subNodeKey), valueId: id, valueFork: fork)
diffs.append(diff)
}
}
func appendDiffs(forSubNode subNodeRef: ZoneReference, fork: Value.Fork) throws {
let refs = try valueReferences(forRootSubNode: subNodeRef)
try appendDiffs(forIdentifiers: refs.map({ $0.valueId }), fork: fork)
}
func appendDiffs(forOriginNode originNode: ZoneReference, onlyBranchNode branchNode: ZoneReference, branch: Value.Fork.Branch) throws {
let vo = try valueReferences(forRootSubNode: originNode)
let vb = try valueReferences(forRootSubNode: branchNode)
let refOById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vo.map({ ($0.valueId, $0) }))
let refBById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vb.map({ ($0.valueId, $0) }))
let allIds = Set(refOById.keys).union(refBById.keys)
for valueId in allIds {
let refO = refOById[valueId]
let refB = refBById[valueId]
switch (refO, refB) {
case let (ro?, rb?):
if ro != rb {
try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: branch.opposite))
} else {
try appendDiffs(forIdentifiers: [valueId], fork: .removed(branch.opposite))
}
case (_?, nil):
try appendDiffs(forIdentifiers: [valueId], fork: .twiceRemoved)
case (nil, _?):
try appendDiffs(forIdentifiers: [valueId], fork: .inserted(branch))
case (nil, nil):
fatalError()
}
}
}
let ref1 = subNodeRefs1ByKey[subNodeKey]
let ref2 = subNodeRefs2ByKey[subNodeKey]
// Skip bucket entirely if both branches reference the same subnode
if ref1 == ref2 { continue }
let origin = refOriginByKey?[subNodeKey]
switch (origin, ref1, ref2) {
case let (o?, r1?, r2?):
let vo = try valueReferences(forRootSubNode: o)
let v1 = try valueReferences(forRootSubNode: r1)
let v2 = try valueReferences(forRootSubNode: r2)
let refOById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: vo.map({ ($0.valueId, $0) }))
let ref1ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v1.map({ ($0.valueId, $0) }))
let ref2ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v2.map({ ($0.valueId, $0) }))
let allIds = Set(refOById.keys).union(ref1ById.keys).union(ref2ById.keys)
for valueId in allIds {
let refO = refOById[valueId]
let ref1 = ref1ById[valueId]
let ref2 = ref2ById[valueId]
switch (refO, ref1, ref2) {
case let (ro?, r1?, r2?):
if ro == r1, ro != r2 {
try appendDiffs(forIdentifiers: [valueId], fork: .updated(.second))
} else if ro != r1, ro == r2 {
try appendDiffs(forIdentifiers: [valueId], fork: .updated(.first))
} else if ro != r1, ro != r2 {
try appendDiffs(forIdentifiers: [valueId], fork: .twiceUpdated)
}
case let (ro?, r1?, nil):
if ro != r1 {
try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: .second))
} else {
try appendDiffs(forIdentifiers: [valueId], fork: .removed(.second))
}
case let (ro?, nil, r2?):
if ro != r2 {
try appendDiffs(forIdentifiers: [valueId], fork: .removedAndUpdated(removedOn: .first))
} else {
try appendDiffs(forIdentifiers: [valueId], fork: .removed(.first))
}
case (nil, _?, _?):
try appendDiffs(forIdentifiers: [valueId], fork: .twiceInserted)
case (nil, nil, _?):
try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.second))
case (nil, _?, nil):
try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.first))
case (_?, nil, nil):
try appendDiffs(forIdentifiers: [valueId], fork: .twiceRemoved)
case (nil, nil, nil):
fatalError()
}
}
case let (o?, r1?, nil):
try appendDiffs(forOriginNode: o, onlyBranchNode: r1, branch: .first)
case let (o?, nil, r2?):
try appendDiffs(forOriginNode: o, onlyBranchNode: r2, branch: .second)
case let (nil, r1?, r2?):
let v1 = try valueReferences(forRootSubNode: r1)
let v2 = try valueReferences(forRootSubNode: r2)
let ref1ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v1.map({ ($0.valueId, $0) }))
let ref2ById: [Value.ID:Value.Reference] = .init(uniqueKeysWithValues: v2.map({ ($0.valueId, $0) }))
let allIds = Set(ref1ById.keys).union(ref2ById.keys)
for valueId in allIds {
let ref1 = ref1ById[valueId]
let ref2 = ref2ById[valueId]
switch (ref1, ref2) {
case (_?, _?):
try appendDiffs(forIdentifiers: [valueId], fork: .twiceInserted)
case (_?, nil):
try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.first))
case (nil, _?):
try appendDiffs(forIdentifiers: [valueId], fork: .inserted(.second))
case (nil, nil):
fatalError()
}
}
case let (nil, r1?, nil):
try appendDiffs(forSubNode: r1, fork: .inserted(.first))
case let (nil, nil, r2?):
try appendDiffs(forSubNode: r2, fork: .inserted(.second))
case let (o?, nil, nil):
try appendDiffs(forSubNode: o, fork: .twiceRemoved)
case (nil, nil, nil):
fatalError()
}
}
return diffs
}
func enumerateValueReferences(forVersionIdentifiedBy versionId: Version.ID, executingForEach block: (Value.Reference) throws -> Void) throws {
let rootRef = ZoneReference(key: rootKey, version: versionId)
guard let rootNode = try node(for: rootRef) else { throw Error.missingVersionRoot }
guard case let .nodes(subNodeRefs) = rootNode.children else { throw Error.missingNode }
try prefetchNodes(for: subNodeRefs)
for subNodeRef in subNodeRefs {
guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }
guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }
for keyValuePair in keyValuePairs {
try block(keyValuePair.valueReference)
}
}
}
func valueReferences(matching key: Map.Key, at version: Version.ID) throws -> [Value.Reference] {
let rootRef = ZoneReference(key: rootKey, version: version)
guard let rootNode = try node(for: rootRef) else { throw Error.missingVersionRoot }
guard case let .nodes(subNodeRefs) = rootNode.children else { throw Error.missingNode }
let subNodeKey = String(key.keyString.prefix(2))
guard let subNodeRef = subNodeRefs.first(where: { $0.key == subNodeKey }) else { return [] }
guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }
guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }
return keyValuePairs.filter({ $0.key == key }).map({ $0.valueReference })
}
fileprivate func node(for key: String, version: Version.ID) throws -> Node? {
let ref = ZoneReference(key: key, version: version)
return try node(for: ref)
}
fileprivate func node(for reference: ZoneReference) throws -> Node? {
try autoreleasepool {
if let node = nodeCache.value(for: reference) {
return node
} else if let data = try zone.data(for: reference) {
let node = try JSONDecoder().decode(Node.self, from: data)
nodeCache.setValue(node, for: reference)
return node
} else {
return nil
}
}
}
private func prefetchNodes(for references: [ZoneReference]) throws {
let uncachedRefs = references.filter { nodeCache.value(for: $0) == nil }
guard !uncachedRefs.isEmpty else { return }
let dataArray = try zone.data(for: uncachedRefs)
let decoder = JSONDecoder()
for (ref, data) in zip(uncachedRefs, dataArray) {
if let data = data, let node = try? decoder.decode(Node.self, from: data) {
nodeCache.setValue(node, for: ref)
}
}
}
private func valueReferences(forRootSubNode subNodeRef: ZoneReference) throws -> [Value.Reference] {
guard let subNode = try node(for: subNodeRef) else { throw Error.missingNode }
guard case let .values(keyValuePairs) = subNode.children else { throw Error.unexpectedNodeContent }
return keyValuePairs.map({ $0.valueReference })
}
}
// MARK:- Subtypes
extension Map {
struct Key: Codable, Hashable {
var keyString: String
init(_ keyString: String = UUID().uuidString) {
self.keyString = keyString
}
}
struct Diff {
var key: Key
var valueId: Value.ID
var valueFork: Value.Fork
}
struct KeyValuePair: Codable, Hashable {
var key: Key
var valueReference: Value.Reference
}
struct Delta {
var key: Key
var addedValueReferences: [Value.Reference] = []
var removedValueIdentifiers: [Value.ID] = []
init(key: Key) {
self.key = key
}
}
struct Node: Codable, Hashable {
var reference: ZoneReference
var children: Children
}
enum Children: Codable, Hashable {
case values([KeyValuePair])
case nodes([ZoneReference])
enum Key: CodingKey {
case values
case nodes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
if let values = try? container.decode([KeyValuePair].self, forKey: .values) {
self = .values(values)
} else if let nodes = try? container.decode([ZoneReference].self, forKey: .nodes) {
self = .nodes(nodes)
} else {
throw Error.encodingFailure("No valid references found in decoder")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
switch self {
case let .values(values):
try container.encode(values, forKey: .values)
case let .nodes(nodes):
try container.encode(nodes, forKey: .nodes)
}
}
}
}
================================================
FILE: Sources/LLVS/Core/Merge.swift
================================================
//
// Merge.swift
// LLVS
//
// Created by Drew McCormack on 19/12/2018.
//
import Foundation
public struct Merge {
public var commonAncestor: Version?
public var versions: (first: Version, second: Version)
public var forksByValueIdentifier: [Value.ID:Value.Fork] = [:]
public init(versions: (first: Version, second: Version), commonAncestor: Version?) {
self.commonAncestor = commonAncestor
self.versions = versions
}
}
public protocol MergeArbiter {
func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change]
}
/// When conflicting, always favor the branch with the most recent version.
public class MostRecentBranchFavoringArbiter: MergeArbiter {
public init() {}
public func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change] {
let v = merge.versions
let favoredBranch: Value.Fork.Branch = v.first.timestamp >= v.second.timestamp ? .first : .second
let favoredVersion = favoredBranch == .first ? v.first : v.second
var changes: [Value.Change] = []
for (valueId, fork) in merge.forksByValueIdentifier {
switch fork {
case let .removedAndUpdated(removeBranch):
if removeBranch == favoredBranch {
changes.append(.preserveRemoval(valueId))
} else {
let value = try store.value(id: valueId, at: favoredVersion.id)!
changes.append(.preserve(value.reference!))
}
case .twiceInserted, .twiceUpdated:
let value = try store.value(id: valueId, at: favoredVersion.id)!
changes.append(.preserve(value.reference!))
case .inserted, .removed, .updated, .twiceRemoved:
break
}
}
return changes
}
}
/// Favors the most recent change on a conflict by conflict basis.
/// Will pick an update over a removal, regardless of recency.
public class MostRecentChangeFavoringArbiter: MergeArbiter {
public init() {}
public func changes(toResolve merge: Merge, in store: Store) throws -> [Value.Change] {
let v = merge.versions
var changes: [Value.Change] = []
for (valueId, fork) in merge.forksByValueIdentifier {
switch fork {
case let .removedAndUpdated(removeBranch):
let favoredVersion = removeBranch.opposite == .first ? v.first : v.second
let value = try store.value(id: valueId, at: favoredVersion.id)!
changes.append(.preserve(value.reference!))
case .twiceInserted, .twiceUpdated:
let value1 = try store.value(id: valueId, at: v.first.id)!
var version1: Version!
store.queryHistory { version1 = $0.version(identifiedBy: value1.storedVersionId!) }
let value2 = try store.value(id: valueId, at: v.second.id)!
var version2: Version!
store.queryHistory { version2 = $0.version(identifiedBy: value2.storedVersionId!) }
if version1.timestamp >= version2.timestamp {
changes.append(.preserve(value1.reference!))
} else {
changes.append(.preserve(value2.reference!))
}
case .inserted, .removed, .updated, .twiceRemoved:
break
}
}
return changes
}
}
================================================
FILE: Sources/LLVS/Core/Snapshot.swift
================================================
//
// Snapshot.swift
// LLVS
//
// Created by Drew McCormack on 09/02/2026.
//
import Foundation
/// Metadata describing a snapshot stored in the cloud.
public struct SnapshotManifest: Codable {
public var snapshotId: String
public var format: String
public var createdAt: Date
public var latestVersionId: Version.ID
public var versionCount: Int
public var chunkCount: Int
public var totalSize: Int64
public init(snapshotId: String = UUID().uuidString, format: String, createdAt: Date = Date(), latestVersionId: Version.ID, versionCount: Int, chunkCount: Int, totalSize: Int64) {
self.snapshotId = snapshotId
self.format = format
self.createdAt = createdAt
self.latestVersionId = latestVersionId
self.versionCount = versionCount
self.chunkCount = chunkCount
self.totalSize = totalSize
}
}
/// Policy controlling automatic snapshot creation after sync.
public struct SnapshotPolicy {
public var enabled: Bool
public var minimumInterval: TimeInterval
public var minimumNewVersions: Int
public init(enabled: Bool, minimumInterval: TimeInterval, minimumNewVersions: Int) {
self.enabled = enabled
self.minimumInterval = minimumInterval
self.minimumNewVersions = minimumNewVersions
}
public static let auto = SnapshotPolicy(
enabled: true, minimumInterval: 7*24*3600, minimumNewVersions: 20
)
public static let disabled = SnapshotPolicy(
enabled: false, minimumInterval: 0, minimumNewVersions: 0
)
}
================================================
FILE: Sources/LLVS/Core/SnapshotCapable+ZIP.swift
================================================
//
// SnapshotCapable+ZIP.swift
// LLVS
//
// Created by Drew McCormack on 01/03/2026.
//
import Foundation
import ZIPFoundation
extension SnapshotCapable {
public var snapshotFormat: String { "zip-v1" }
public func writeSnapshotChunks(storeRootURL: URL, to directory: URL, maxChunkSize: Int) throws -> SnapshotManifest {
let fm = FileManager()
try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
// Create zip of the entire store directory
let zipURL = directory.appendingPathComponent("snapshot.zip")
try fm.zipItem(at: storeRootURL, to: zipURL, shouldKeepParent: false, compressionMethod: .deflate)
defer { try? fm.removeItem(at: zipURL) }
// Get total compressed size
let zipAttributes = try fm.attributesOfItem(atPath: zipURL.path)
let totalSize = (zipAttributes[.size] as? Int64) ?? 0
// Split zip into chunks using FileHandle for streaming
let readHandle = try FileHandle(forReadingFrom: zipURL)
defer { try? readHandle.close() }
var chunkIndex = 0
var bytesRemaining = totalSize
while bytesRemaining > 0 {
let bytesToRead = min(Int(bytesRemaining), maxChunkSize)
let chunkData = readHandle.readData(ofLength: bytesToRead)
if chunkData.isEmpty { break }
let chunkFile = directory.appendingPathComponent(String(format: "chunk-%03d", chunkIndex))
try chunkData.write(to: chunkFile)
chunkIndex += 1
bytesRemaining -= Int64(chunkData.count)
}
// Scan versions/ for manifest metadata
let versionsDir = storeRootURL.appendingPathComponent("versions")
var versionCount = 0
var latestVersionId = Version.ID("")
var maxTimestamp: TimeInterval = 0
if let versionsEnum = fm.enumerator(at: versionsDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) {
for case let fileURL as URL in versionsEnum {
guard fileURL.pathExtension == "json" else { continue }
var isDir: ObjCBool = false
guard fm.fileExists(atPath: fileURL.path, isDirectory: &isDir), !isDir.boolValue else { continue }
versionCount += 1
if let data = try? Data(contentsOf: fileURL),
let version = try? JSONDecoder().decode(Version.self, from: data),
version.timestamp > maxTimestamp {
maxTimestamp = version.timestamp
latestVersionId = version.id
}
}
}
return SnapshotManifest(
format: snapshotFormat,
latestVersionId: latestVersionId,
versionCount: versionCount,
chunkCount: chunkIndex,
totalSize: totalSize
)
}
public func restoreFromSnapshotChunks(storeRootURL: URL, from directory: URL, manifest: SnapshotManifest) throws {
let fm = FileManager()
// Concatenate chunks into a zip file
let zipURL = directory.appendingPathComponent("snapshot.zip")
fm.createFile(atPath: zipURL.path, contents: nil)
let writeHandle = try FileHandle(forWritingTo: zipURL)
for i in 0..<manifest.chunkCount {
let chunkFile = directory.appendingPathComponent(String(format: "chunk-%03d", i))
let chunkData = try Data(contentsOf: chunkFile)
writeHandle.write(chunkData)
}
try writeHandle.close()
// Unzip to store root
try fm.unzipItem(at: zipURL, to: storeRootURL)
try? fm.removeItem(at: zipURL)
}
}
================================================
FILE: Sources/LLVS/Core/Storage.swift
================================================
//
// Storage.swift
// LLVS
//
// Created by Drew McCormack on 14/05/2019.
//
import Foundation
public enum MapType {
case valuesByVersion // Main map for identifying which values are in each version
case userDefined(label: String)
}
public protocol Storage {
func makeValuesZone(in store: Store) throws -> Zone
func makeMapZone(for type: MapType, in store: Store) throws -> Zone
}
/// Storage backends that can produce and consume chunked snapshots.
public protocol SnapshotCapable {
var snapshotFormat: String { get }
func writeSnapshotChunks(
storeRootURL: URL, to directory: URL, maxChunkSize: Int
) throws -> SnapshotManifest
func restoreFromSnapshotChunks(
storeRootURL: URL, from directory: URL, manifest: SnapshotManifest
) throws
}
================================================
FILE: Sources/LLVS/Core/Store.swift
================================================
//
// Store.swift
// llvs
//
// Created by Drew McCormack on 31/10/2018.
//
import Foundation
import Synchronization
// MARK:- Branch
public struct Branch: RawRepresentable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
public init(randomizedNameBasedOn base: String = "") {
let separator = base.isEmpty ? "" : "_"
self.rawValue = "\(base)\(separator)\(UUID().uuidString)"
}
}
// MARK:- Store
public final class Store {
public enum Error: Swift.Error {
case missingVersion
case attemptToLocateUnversionedValue
case attemptToStoreValueWithNoVersion
case noCommonAncestor(firstVersion: Version.ID, secondVersion: Version.ID)
case unresolvedConflict(valueId: Value.ID, valueFork: Value.Fork)
case attemptToAddExistingVersion(Version.ID)
case attemptToAddVersionWithNonexistingPredecessors(Version)
}
public let rootDirectoryURL: URL
public let valuesDirectoryURL: URL
public let versionsDirectoryURL: URL
public let mapsDirectoryURL: URL
public let valuesMapDirectoryURL: URL
public let storage: Storage
private lazy var valuesZone: Zone = {
return try! storage.makeValuesZone(in: self)
}()
private let valuesMapName = "__llvs_values"
private lazy var valuesMap: Map = {
let valuesMapZone = try! self.storage.makeMapZone(for: .valuesByVersion, in: self)
return Map(zone: valuesMapZone)
}()
private let history = Mutex(History())
fileprivate let fileManager = FileManager()
public init(rootDirectoryURL: URL, storage: Storage = FileStorage()) throws {
self.storage = storage
self.rootDirectoryURL = rootDirectoryURL.resolvingSymlinksInPath()
self.valuesDirectoryURL = rootDirectoryURL.appendingPathComponent("values")
self.versionsDirectoryURL = rootDirectoryURL.appendingPathComponent("versions")
self.mapsDirectoryURL = rootDirectoryURL.appendingPathComponent("maps")
self.valuesMapDirectoryURL = self.mapsDirectoryURL.appendingPathComponent(valuesMapName)
try? fileManager.createDirectory(at: self.rootDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: self.valuesDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: self.versionsDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: self.mapsDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try reloadHistory()
}
/// Call this to make sure all history is loaded. For example, if the store could have been
/// changed by another process, calling this method will ensure the versions added by that process
/// are loaded.
public func reloadHistory() throws {
try history.withLock { history in
var newVersions: Set<Version> = []
for version in try storedVersions() where history.version(identifiedBy: version.id) == nil {
newVersions.insert(version)
try history.add(version, updatingPredecessorVersions: false)
}
for version in newVersions {
try history.updateSuccessors(inPredecessorsOf: version)
}
}
}
/// Provides access to the history object in a serialized way, allowing access from any thread.
/// Calls the block passed after getting exclusive history to the history object, and passes the history.
public func queryHistory(in block: (History) throws ->Void) rethrows {
try history.withLock { history in
try block(history)
}
}
public func historyIncludesVersions(identifiedBy versionIds: [Version.ID]) -> Bool {
var valid = false
queryHistory { history in
valid = versionIds.allSatisfy { history.version(identifiedBy: $0) != nil }
}
return valid
}
}
// MARK:- Storing Values and Versions
extension Store {
/// Convenience to avoid having to create Value.Change values yourself
@discardableResult public func makeVersion(basedOnPredecessor versionId: Version.ID?, inserting insertedValues: [Value] = [], updating updatedValues: [Value] = [], removing removedIds: [Value.ID] = [], metadata: Version.Metadata = [:]) throws -> Version {
let predecessors = versionId.flatMap { Version.Predecessors(idOfFirst: $0, idOfSecond: nil) }
let inserts: [Value.Change] = insertedValues.map { .insert($0) }
let updates: [Value.Change] = updatedValues.map { .update($0) }
let removes: [Value.Change] = removedIds.map { .remove($0) }
return try makeVersion(basedOn: predecessors, storing: inserts+updates+removes, metadata: metadata)
}
@discardableResult public func makeVersion(basedOnPredecessor version: Version.ID?, storing changes: [Value.Change], metadata: Version.Metadata = [:]) throws -> Version {
let predecessors = version.flatMap { Version.Predecessors(idOfFirst: $0, idOfSecond: nil) }
return try makeVersion(basedOn: predecessors, storing: changes, metadata: metadata)
}
/// Changes must include all updates to the map of the first predecessor. If necessary, preserves should be included to bring values
/// from the second predecessor into the first predecessor map.
@discardableResult internal func makeVersion(basedOn predecessors: Version.Predecessors?, storing changes: [Value.Change], metadata: Version.Metadata = [:]) throws -> Version {
let version = Version(predecessors: predecessors, valueDataSize: changes.valueDataSize, metadata: metadata)
try addVersion(version, storing: changes)
return version
}
/// This method does not check consistency, and does not automatically update the map.
/// It is assumed that any changes to the first predecessor that are needed in the map
/// are present as preserves from the second predecessor.
internal func addVersion(_ version: Version, storing changes: [Value.Change]) throws {
guard !historyIncludesVersions(identifiedBy: [version.id]) else {
throw Error.attemptToAddExistingVersion(version.id)
}
guard historyIncludesVersions(identifiedBy: version.predecessors?.ids ?? []) else {
throw Error.attemptToAddVersionWithNonexistingPredecessors(version)
}
// Store values
var valueDataSize: Int64 = 0
for change in changes {
switch change {
case .insert(let value), .update(let value):
var newValue = value
newValue.storedVersionId = version.id
try self.store(newValue)
valueDataSize += Int64(newValue.data.count)
case .remove, .preserve, .preserveRemoval:
continue
}
}
// Update values map
let deltas: [Map.Delta] = changes.map { change in
switch change {
case .insert(let value), .update(let value):
let valueRef = Value.Reference(valueId: value.id, storedVersionId: version.id)
var delta = Map.Delta(key: Map.Key(value.id.rawValue))
delta.addedValueReferences = [valueRef]
return delta
case .remove(let valueId), .preserveRemoval(let valueId):
var delta = Map.Delta(key: Map.Key(valueId.rawValue))
delta.removedValueIdentifiers = [valueId]
return delta
case .preserve(let valueRef):
var delta = Map.Delta(key: Map.Key(valueRef.valueId.rawValue))
delta.addedValueReferences = [valueRef]
return delta
}
}
try valuesMap.addVersion(version.id, basedOn: version.predecessors?.idOfFirst, applying: deltas)
// Store version
var versionWithDataSize = version
versionWithDataSize.valueDataSize = valueDataSize
try store(versionWithDataSize)
// Add to history
try history.withLock { history in
do {
try history.add(version, updatingPredecessorVersions: true)
} catch History.Error.attemptToAddPreexistingVersion {
throw Error.attemptToAddExistingVersion(version.id)
}
}
}
private func store(_ value: Value) throws {
guard let zoneRef = value.zoneReference else { throw Error.attemptToStoreValueWithNoVersion }
try valuesZone.store(value.data, for: zoneRef)
}
}
// MARK:- Fetching Values
extension Store {
public func valueReference(id valueId: Value.ID, at versionId: Version.ID) throws -> Value.Reference? {
return try valuesMap.valueReferences(matching: .init(valueId.rawValue), at: versionId).first
}
public func valueReferences(at version: Version.ID) throws -> [Value.Reference] {
var refs: [Value.Reference] = []
try enumerate(version: version) { ref in
refs.append(ref)
}
return refs
}
/// Convenient method to avoid having to create id types
public func value(idString valueIdString: String, at versionId: Version.ID) throws -> Value? {
return try value(id: .init(valueIdString), at: versionId)
}
public func value(id valueId: Value.ID, at versionId: Version.ID) throws -> Value? {
let ref = try valueReference(id: valueId, at: versionId)
return try ref.flatMap { try value(id: valueId, storedAt: $0.storedVersionId) }
}
public func value(id valueId: Value.ID, storedAt versionId: Version.ID) throws -> Value? {
guard let data = try valuesZone.data(for: .init(key: valueId.rawValue, version: versionId)) else { return nil }
let value = Value(id: valueId, storedVersionId: versionId, data: data)
return value
}
public func value(storedAt valueReference: Value.Reference) throws -> Value? {
return try value(id: valueReference.valueId, storedAt: valueReference.storedVersionId)
}
public func enumerate(version versionId: Version.ID, executingForEach block: (Value.Reference) throws -> Void) throws {
try valuesMap.enumerateValueReferences(forVersionIdentifiedBy: versionId, executingForEach: block)
}
}
// MARK:- Merging
extension Store {
public enum MergeHeadSelection {
case all
case allExceptBranches
case allUnbranchedAndSpecificBranches([Branch])
case specificBranchesOnly([Branch])
}
/// Whether there is more than one head
public var hasMultipleHeads: Bool {
var result: Bool = false
queryHistory { history in
result = history.headIdentifiers.count > 1
}
return result
}
/// Finds any heads that have the branch in the metadata.
public func heads(withBranch branch: Branch) -> [Version.ID] {
var result: [Version.ID] = []
queryHistory { history in
let heads = history.headIdentifiers
result = heads.filter { id in
let version = history.version(identifiedBy: id)!
return branch.rawValue == version.metadata[.branch]?.value()
}
}
return result
}
/// Merges heads into the version passed, which is usually a head itself. This is a convenience
/// to save looping through all heads.
/// If the version ends up being changed by the merging, the new version is returned, otherwise nil.
public func mergeHeads(into version: Version.ID, resolvingWith arbiter: MergeArbiter, headSelection: MergeHeadSelection = .allExceptBranches, metadata: Version.Metadata = [:]) -> Version.ID? {
var heads: Set<Version.ID> = []
var versionsById: [Version.ID:Version] = [:]
queryHistory { history in
heads = history.headIdentifiers
versionsById = .init(uniqueKeysWithValues: heads.map({ ($0, history.version(identifiedBy: $0)!) }))
}
heads.remove(version)
heads = heads.filter { id in
let version = versionsById[id]!
let branch: String? = version.metadata[.branch]?.value()
switch headSelection {
case .all:
return true
case .allExceptBranches:
return branch == nil
case .allUnbranchedAndSpecificBranches(let branches):
return branch == nil || branches.map({ $0.rawValue }).contains(branch)
case .specificBranchesOnly(let branches):
return branches.map({ $0.rawValue }).contains(branch)
}
}
guard !heads.isEmpty else { return nil }
var versionId: Version.ID = version
for otherHead in heads {
let newVersion = try! merge(version: versionId, with: otherHead, resolvingWith: arbiter, metadata: metadata)
versionId = newVersion.id
}
return versionId
}
/// Will choose between a three way merge, and a two way merge, based on whether a common ancestor is found.
public func merge(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {
do {
return try mergeRelated(version: firstVersionIdentifier, with: secondVersionIdentifier, resolvingWith: arbiter, metadata: metadata)
} catch Error.noCommonAncestor {
return try mergeUnrelated(version: firstVersionIdentifier, with: secondVersionIdentifier, resolvingWith: arbiter, metadata: metadata)
}
}
/// Two-way merge between two versions that have no common ancestry. Effectively we assume an empty common ancestor,
/// so that all changes are inserts, or conflicting twiceInserts.
public func mergeUnrelated(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {
var firstVersion, secondVersion: Version?
var fastForwardVersion: Version?
try history.withLock { history in
firstVersion = history.version(identifiedBy: firstVersionIdentifier)
secondVersion = history.version(identifiedBy: secondVersionIdentifier)
guard firstVersion != nil, secondVersion != nil else {
throw Error.missingVersion
}
// Check for fast forward
if history.isAncestralLine(from: firstVersion!.id, to: secondVersion!.id) {
fastForwardVersion = secondVersion
} else if history.isAncestralLine(from: secondVersion!.id, to: firstVersion!.id) {
fastForwardVersion = firstVersion
}
}
if let fastForwardVersion = fastForwardVersion {
return fastForwardVersion
}
return try merge(firstVersion!, and: secondVersion!, withCommonAncestor: nil, resolvingWith: arbiter, metadata: metadata)
}
/// Three-way merge between two versions, and a common ancestor. If no common ancestor is found, a .noCommonAncestor error is thrown.
/// Conflicts are resolved using the MergeArbiter passed in.
public func mergeRelated(version firstVersionIdentifier: Version.ID, with secondVersionIdentifier: Version.ID, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {
var firstVersion, secondVersion, commonVersion: Version?
var commonVersionIdentifier: Version.ID?
try history.withLock { history in
commonVersionIdentifier = try history.greatestCommonAncestor(ofVersionsIdentifiedBy: (firstVersionIdentifier, secondVersionIdentifier))
guard commonVersionIdentifier != nil else {
throw Error.noCommonAncestor(firstVersion: firstVersionIdentifier, secondVersion: secondVersionIdentifier)
}
firstVersion = history.version(identifiedBy: firstVersionIdentifier)
secondVersion = history.version(identifiedBy: secondVersionIdentifier)
commonVersion = history.version(identifiedBy: commonVersionIdentifier!)
guard firstVersion != nil, secondVersion != nil else {
throw Error.missingVersion
}
}
// Check for fast forward cases where no merge is needed
if firstVersionIdentifier == commonVersionIdentifier {
return secondVersion!
} else if secondVersionIdentifier == commonVersionIdentifier {
return firstVersion!
}
return try merge(firstVersion!, and: secondVersion!, withCommonAncestor: commonVersion!, resolvingWith: arbiter, metadata: metadata)
}
/// Two or three-way merge. Does no check to see if fast forwarding is possible. Will carry out the merge regardless of history.
/// If a common ancestor is supplied, it is 3-way, and otherwise 2-way.
private func merge(_ firstVersion: Version, and secondVersion: Version, withCommonAncestor commonAncestor: Version?, resolvingWith arbiter: MergeArbiter, metadata: Version.Metadata = [:]) throws -> Version {
// Prepare merge
let predecessors = Version.Predecessors(idOfFirst: firstVersion.id, idOfSecond: secondVersion.id)
let diffs = try valuesMap.differences(between: firstVersion.id, and: secondVersion.id, withCommonAncestor: commonAncestor?.id)
var merge = Merge(versions: (firstVersion, secondVersion), commonAncestor: commonAncestor)
let forkTuples = diffs.map({ ($0.valueId, $0.valueFork) })
merge.forksByValueIdentifier = .init(uniqueKeysWithValues: forkTuples)
// Resolve with arbiter
var changes = try arbiter.changes(toResolve: merge, in: self)
// Check changes resolve conflicts
let idsInChanges = Set(changes.valueIds)
for diff in diffs {
if diff.valueFork.isConflicting && !idsInChanges.contains(diff.valueId) {
throw Error.unresolvedConflict(valueId: diff.valueId, valueFork: diff.valueFork)
}
}
// Must make sure any change that was made in the second predecessor is included,
// via a 'preserve' if necessary.
// This is so the map of the first predecessor is updated properly.
for diff in diffs where !idsInChanges.contains(diff.valueId) {
switch diff.valueFork {
case .inserted(let branch) where branch == .second:
fallthrough
case .updated(let branch) where branch == .second:
let ref = try valueReference(id: diff.valueId, at: secondVersion.id)!
changes.append(.preserve(ref))
case .removed(let branch) where branch == .second:
changes.append(.preserveRemoval(diff.valueId))
default:
break
}
}
return try makeVersion(basedOn: predecessors, storing: changes, metadata: metadata)
}
}
// MARK:- Value Changes
extension Store {
/// Returns the changes actually made in the version passed. This is important for an exchange, for example, that wishes to
/// store a set of changes. Note that it is not exactly equivalent to taking the diff between the version and one of its predecessors,
/// because in that case, any changes made in the branch of the other predecessor will also be included as changes, when they don't
/// really belong (ie they were actually made in the past)
public func valueChanges(madeInVersionIdentifiedBy versionId: Version.ID) throws -> [Value.Change] {
guard let version = try version(identifiedBy: versionId) else { throw Error.missingVersion }
guard let predecessors = version.predecessors else {
var changes: [Value.Change] = []
try valuesMap.enumerateValueReferences(forVersionIdentifiedBy: versionId) { ref in
let v = try value(id: ref.valueId, storedAt: ref.storedVersionId)!
changes.append(.insert(v))
}
return changes
}
var changes: [Value.Change] = []
let p1 = predecessors.idOfFirst
if let p2 = predecessors.idOfSecond {
// Do a reverse-in-time fork, and negate the outcome
let diffs = try valuesMap.differences(between: p1, and: p2, withCommonAncestor: versionId)
for diff in diffs {
switch diff.valueFork {
case .twiceInserted:
changes.append(.remove(diff.valueId))
case .twiceUpdated, .removedAndUpdated:
let value = try self.value(id: diff.valueId, at: versionId)!
changes.append(.update(value))
case .twiceRemoved:
let value = try self.value(id: diff.valueId, at: versionId)!
changes.append(.insert(value))
case .inserted:
changes.append(.preserveRemoval(diff.valueId))
case .removed, .updated:
let value = try self.value(id: diff.valueId, at: versionId)!
changes.append(.preserve(value.reference!))
}
}
} else {
changes = try valueChanges(madeBetween: p1, and: version.id)
}
return changes
}
/// Changes that can be applied to go from the first version to the second. Useful for "diffing", eg, updating UI by seeing what changed.
public func valueChanges(madeBetween versionId1: Version.ID, and versionId2: Version.ID) throws -> [Value.Change] {
guard let _ = try version(identifiedBy: versionId1), let _ = try version(identifiedBy: versionId2) else { throw Error.missingVersion }
var changes: [Value.Change] = []
let diffs = try valuesMap.differences(between: versionId2, and: versionId1, withCommonAncestor: versionId1)
for diff in diffs {
switch diff.valueFork {
case .inserted:
let value = try self.value(id: diff.valueId, at: versionId2)!
changes.append(.insert(value))
case .removed:
changes.append(.remove(diff.valueId))
case .updated:
let value = try self.value(id: diff.valueId, at: versionId2)!
changes.append(.update(value))
case .removedAndUpdated, .twiceInserted, .twiceRemoved, .twiceUpdated:
fatalError("Should not be possible with only a single branch")
}
}
return changes
}
}
// MARK:- Storing and Fetching Versions
extension Store {
fileprivate func store(_ version: Version) throws {
try autoreleasepool {
let (dir, file) = fileSystemLocation(forVersionIdentifiedBy: version.id)
try? fileManager.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
let data = try JSONEncoder().encode(version)
try data.write(to: file)
}
}
/// Returns all versions of a value with the given identifier in the history.
/// Order is topological, from recent to ancient. No timestamp ordering has been applied
/// This can be expensive, as it iterates all history.
public func versionIds(for valueId: Value.ID) throws -> [Version.ID] {
var existingVersions: Set<Version.ID> = []
var valueVersions: [Version.ID] = []
try queryHistory { history in
for v in history {
if let ref = try valueReference(id: valueId, at: v.id), !existingVersions.contains(ref.storedVersionId) {
valueVersions.append(ref.storedVersionId)
existingVersions.insert(ref.storedVersionId)
}
}
}
return valueVersions
}
/// Version ids found in store. This makes no use of the loaded history.
internal func storedVersionIds(for valueId: Value.ID) throws -> [Version.ID] {
let valueDirectoryURL = valuesDirectoryURL.appendingSplitPathComponent(valueId.rawValue)
let enumerator = fileManager.enumerator(at: valueDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!
let valueDirComponents = valueDirectoryURL.standardizedFileURL.pathComponents
var versionIds: [Version.ID] = []
for any in enumerator {
var isDirectory: ObjCBool = true
guard let url = any as? URL else { continue }
guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }
guard url.pathExtension == "json" else { continue }
let allComponents = url.standardizedFileURL.deletingPathExtension().pathComponents
let versionComponents = allComponents[valueDirComponents.count...]
let versionString = versionComponents.joined()
versionIds.append(.init(versionString))
}
return versionIds
}
/// Versions found in store. This makes no use of the loaded history.
fileprivate func storedVersions() throws -> [Version] {
try autoreleasepool {
let enumerator = fileManager.enumerator(at: versionsDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])!
var versions: [Version] = []
let decoder = JSONDecoder()
for any in enumerator {
var isDirectory: ObjCBool = true
guard let url = any as? URL else { continue }
guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue else { continue }
guard url.pathExtension == "json" else { continue }
let data = try Data(contentsOf: url)
let version = try decoder.decode(Version.self, from: data)
versions.append(version)
}
return versions
}
}
public func version(identifiedBy versionId: Version.ID) throws -> Version? {
var version: Version?
queryHistory { history in
version = history.version(identifiedBy: versionId)
}
return version
}
public var mostRecentHead: Version? {
var version: Version?
queryHistory { history in
version = history.mostRecentHead
}
return version
}
}
// MARK:- File System Locations
fileprivate extension Store {
func fileSystemLocation(forVersionIdentifiedBy identifier: Version.ID) -> (directoryURL: URL, fileURL: URL) {
let fileURL = versionsDirectoryURL.appendingSplitPathComponent(identifier.rawValue).appendingPathExtension("json")
let directoryURL = fileURL.deletingLastPathComponent()
return (directoryURL: directoryURL, fileURL: fileURL)
}
}
// MARK:- Path Utilities
internal extension URL {
/// Appends a path to the messaged URL that consists of a filename for which
/// a prefix is taken as a subdirectory. Eg. `file:///root` might become
/// `file:///root/fi/lename.jpg` when appending `filename.jpg` with `subDirectoryNameLength` of 2.
func appendingSplitPathComponent(_ name: String, prefixLength: UInt = 2) -> URL {
guard name.count > prefixLength else {
return appendingPathComponent(name)
}
// Embed a subdirectory
let index = name.index(name.startIndex, offsetBy: Int(prefixLength))
let prefix = String(name[..<index])
let postfix = String(name[index...])
let directory = appendingPathComponent(prefix).appendingPathComponent(postfix)
return directory
}
}
================================================
FILE: Sources/LLVS/Core/StoreCoordinator.swift
================================================
//
// StoreCoordinator.swift
// LLVS
//
// Created by Drew McCormack on 09/06/2019.
// Copyright © 2019 Momenta B.V. All rights reserved.
//
import Foundation
import Synchronization
/// A `StoreCoordinator` takes care of all aspects of setting up a syncing store.
/// It's the simplest way to get started, though you may want more control for advanced use cases.
///
/// Thread-safety: `currentVersion` is protected by a `Mutex`. Other mutable properties
/// (`exchange`, `mergeArbiter`, `defaultMetadataForNewVersions`, `isExchanging`) are
/// set during initialization or within the serialized exchange flow.
public class StoreCoordinator: @unchecked Sendable {
private struct CachedData: Codable {
var exchangeRestorationData: Data?
var currentVersionIdentifier: Version.ID
}
public let store: Store
public let snapshotPolicy: SnapshotPolicy
public var exchange: (any Exchange)? {
didSet {
exchange?.restorationState = cachedData?.exchangeRestorationData
}
}
public var mergeArbiter: MergeArbiter = MostRecentChangeFavoringArbiter()
public var defaultMetadataForNewVersions: Version.Metadata = [:]
public let storeDirectoryURL: URL
public let cacheDirectoryURL: URL
private var cachedCoordinatorFileURL: URL
public var exchangeRestorationData: Data? {
return exchange?.restorationState
}
public private(set) var currentVersionUpdates: AsyncStream<Version.ID>
private var currentVersionContinuation: AsyncStream<Version.ID>.Continuation?
private let _currentVersion: Mutex<Version.ID>
public var currentVersion: Version.ID {
_currentVersion.withLock { $0 }
}
private func updateCurrentVersion(_ newValue: Version.ID) {
let changed = _currentVersion.withLock { current -> Bool in
guard current != newValue else { return false }
current = newValue
return true
}
if changed {
persist()
currentVersionContinuation?.yield(newValue)
}
}
private class var defaultStoreDirectory: URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let rootDir = appSupport.appendingPathComponent("LLVS").appendingPathComponent("DefaultStore")
return rootDir
}
private class var defaultCacheDirectory: URL {
let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let rootDir = cachesDir.appendingPathComponent("LLVS").appendingPathComponent("CoordinatorCache")
return rootDir
}
/// This will setup a store in the default location (Applicaton Support). If you need more than one store,
/// use `init(withStoreDirectoryAt:,cacheDirectoryAt:)` instead.
public convenience init(snapshotPolicy: SnapshotPolicy = .disabled) throws {
try self.init(withStoreDirectoryAt: Self.defaultStoreDirectory, cacheDirectoryAt: Self.defaultStoreDirectory, snapshotPolicy: snapshotPolicy)
}
/// Gives full control over where the store is (directory location), and where cached data should be kept (directory).
/// The directories will be created if they do not exist.
public init(withStoreDirectoryAt storeURL: URL, cacheDirectoryAt coordinatorCacheURL: URL, snapshotPolicy: SnapshotPolicy = .disabled) throws {
self.snapshotPolicy = snapshotPolicy
self.storeDirectoryURL = storeURL
self.cacheDirectoryURL = coordinatorCacheURL
self.cachedCoordinatorFileURL = cacheDirectoryURL.appendingPathComponent("Coordinator.json")
try FileManager.default.createDirectory(at: storeURL, withIntermediateDirectories: true, attributes: nil)
try FileManager.default.createDirectory(at: coordinatorCacheURL, withIntermediateDirectories: true, attributes: nil)
self.store = try Store(rootDirectoryURL: storeURL)
self._currentVersion = Mutex(Version.ID()) // Set a temporary version. Final is in cache
var continuation: AsyncStream<Version.ID>.Continuation?
self.currentVersionUpdates = AsyncStream { continuation = $0 }
self.currentVersionContinuation = continuation
try loadCache()
}
private func loadCache() throws {
// Load state from cache
let cachedData: CachedData
var shouldPersist = false
if let cached = self.cachedData {
cachedData = cached
} else {
// Get most recent, or make first commit
let version: Version.ID
if let head = store.mostRecentHead {
version = head.id
} else {
version = try store.makeVersion(basedOnPredecessor: nil, storing: []).id
}
cachedData = CachedData(currentVersionIdentifier: version)
shouldPersist = true
}
// Set properties from cache
updateCurrentVersion(cachedData.currentVersionIdentifier)
if shouldPersist { persist() }
}
private var cachedData: CachedData? {
let fileManager = FileManager()
if fileManager.fileExists(atPath: self.cachedCoordinatorFileURL.path),
let data = try? Data(contentsOf: self.cachedCoordinatorFileURL),
let cached = try? JSONDecoder().decode(CachedData.self, from: data) {
return cached
} else {
return nil
}
}
/// Store cached data
private func persist() {
let cachedData = CachedData(exchangeRestorationData: exchange?.restorationState, currentVersionIdentifier: currentVersion)
if let data = try? JSONEncoder().encode(cachedData) {
try? data.write(to: cachedCoordinatorFileURL)
}
}
// MARK: Heads
public func heads(withBranch branch: Branch) -> [Version.ID] {
store.heads(withBranch: branch)
}
/// Will attempt to get the first branch version, if one exists; if not, will return the current version.
/// This is just a convenient way to get a base version.
public func versionForBranchOrCurrentHead(for branch: Branch? = nil) -> Version.ID {
branch.flatMap({ heads(withBranch: $0).first }) ?? currentVersion
}
// MARK: Saving
/// You should use this to save instead of using the store directly, so that the
/// coordinator can track versions. Otherwise you will need to merge to see the changes.
public func save(_ changes: [Value.Change], in branch: Branch? = nil, metadata: Version.Metadata? = nil) throws {
guard !changes.isEmpty || metadata != nil else { return }
var metadata = metadata ?? defaultMetadataForNewVersions
if let branch = branch { metadata[.branch] = .init(branch.rawValue) }
updateCurrentVersion(try store.makeVersion(basedOnPredecessor: versionForBranchOrCurrentHead(for: branch), storing: changes, metadata: metadata).id)
}
public func save(inserting inserts: [Value] = [], updating updates: [Value] = [], removing removals: [Value.ID] = [], in branch: Branch? = nil, metadata: Version.Metadata? = nil) throws {
guard !inserts.isEmpty || !updates.isEmpty || !removals.isEmpty || metadata != nil else { return }
var metadata = metadata ?? defaultMetadataForNewVersions
if let branch = branch { metadata[.branch] = .init(branch.rawValue) }
updateCurrentVersion(try store.makeVersion(basedOnPredecessor: versionForBranchOrCurrentHead(for: branch), inserting: inserts, updating: updates, removing: removals, metadata: metadata).id)
}
// MARK: Fetching
/// Pass a specific version, or nil for the current version
public func valueReferences(at version: Version.ID? = nil) throws -> [Value.Reference] {
try store.valueReferences(at: version ?? currentVersion)
}
public func values(at version: Version.ID? = nil) throws -> [Value] {
try autoreleasepool {
return try valueReferences(at: version).map { try store.value(storedAt: $0)! }
}
}
public func value(idString: String) throws -> Value? {
return try store.value(idString: idString, at: currentVersion)
}
public func value(id: Value.ID) throws -> Value? {
return try store.value(id: id, at: currentVersion)
}
public func value(idString: String, on branch: Branch?) throws -> Value? {
return try store.value(idString: idString, at: versionForBranchOrCurrentHead(for: branch))
}
public func value(id: Value.ID, on branch: Branch?) throws -> Value? {
return try store.value(id: id, at: versionForBranchOrCurrentHead(for: branch))
}
// MARK: Sync
public private(set) var isExchanging = false
/// Serializer to ensure one exchange at a time.
private var exchangeSerializer = ExchangeSerializer()
/// This transfers data between cloud and local store, but does not alter the current branch or do any merging.
/// It's a bit like a two-way version of Git's fetch.
public func exchange() async throws {
try await exchangeSerializer.enqueue { [self] in
try await self.performExchange()
}
}
private func performExchange() async throws {
isExchanging = true
defer { isExchanging = false }
guard let exchange = exchange else { return }
let _ = try await exchange.retrieve()
let _ = try await exchange.send()
// Upload snapshot if policy allows (fire and forget)
Task { [weak self] in
try? await self?.uploadSnapshotIfNeeded()
}
}
/// Merging any extra heads, or fast forward to latest. It's a good idea to save data just before calling this, so that
/// in view edits are committed. Returns true if the merge changed the current version; false otherwise.
/// 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.
@discardableResult public func merge(metadata: Version.Metadata? = nil, headSelection: Store.MergeHeadSelection = .allExceptBranches) -> Bool {
let metadata = metadata ?? defaultMetadataForNewVersions
let newVersion = self.store.mergeHeads(into: self.currentVersion, resolvingWith: self.mergeArbiter, headSelection: headSelection, metadata: metadata)
if let newVersion = newVersion {
updateCurrentVersion(newVersion)
return true
} else {
return false
}
}
// MARK: Snapshots
/// Download and restore a cloud snapshot if available and compatible.
/// Call before the first exchange on a new device.
public func bootstrapFromSnapshot() async throws {
guard let snapshotExchange = exchange as? SnapshotExchange,
let snapshotStorage = store.storage as? SnapshotCapable else {
return
}
// Check store has minimal history (≤ 1 version) — skip if already populated
var versionCount = 0
store.queryHistory { history in
versionCount = history.allVersionIdentifiers.count
}
guard versionCount <= 1 else { return }
guard let manifest = try await snapshotExchange.retrieveSnapshotManifest() else { return }
// Check format matches
guard manifest.format == snapshotStorage.snapshotFormat else { return }
// Download chunks to temp directory
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
defer { try? FileManager.default.removeItem(at: tempDir) }
for i in 0..<manifest.chunkCount {
let data = try await snapshotExchange.retrieveSnapshotChunk(index: i)
let chunkFile = tempDir.appendingPathComponent(String(format: "chunk-%03d", i))
try data.write(to: chunkFile)
}
try snapshotStorage.restoreFromSnapshotChunks(
storeRootURL: self.store.rootDirectoryURL,
from: tempDir,
manifest: manifest
)
try self.store.reloadHistory()
// Update currentVersion to the latest head
if let head = self.store.mostRecentHead {
updateCurrentVersion(head.id)
}
}
internal func uploadSnapshotIfNeeded() async throws {
guard snapshotPolicy.enabled,
let snapshotExchange = exchange as? SnapshotExchange,
let snapshotStorage = store.storage as? SnapshotCapable else {
return
}
// Check current version count
var currentVersionCount = 0
store.queryHistory { history in
currentVersionCount = history.allVersionIdentifiers.count
}
let existingManifest = try await snapshotExchange.retrieveSnapshotManifest()
if let manifest = existingManifest {
// Skip if recent enough
if manifest.createdAt.timeIntervalSinceNow > -self.snapshotPolicy.minimumInterval {
return
}
// Skip if not enough new versions
if currentVersionCount - manifest.versionCount < self.snapshotPolicy.minimumNewVersions {
return
}
}
// Write snapshot to temp directory
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
defer { try? FileManager.default.removeItem(at: tempDir) }
let manifest = try snapshotStorage.writeSnapshotChunks(
storeRootURL: self.store.rootDirectoryURL,
to: tempDir,
maxChunkSize: 5_000_000
)
try await snapshotExchange.sendSnapshot(manifest: manifest, chunkProvider: { index in
let chunkFile = tempDir.appendingPathComponent(String(format: "chunk-%03d", index))
return try Data(contentsOf: chunkFile)
})
}
}
// MARK: - ExchangeSerializer
/// Ensures only one exchange runs at a time using an AsyncStream as a FIFO queue.
private final class ExchangeSerializer: @unchecked Sendable {
private typealias Work = @Sendable () async throws -> Void
private let stream: AsyncStream<Work>
private let continuation: AsyncStream<Work>.Continuation
init() {
(stream, continuation) = AsyncStream<Work>.makeStream()
Task { [stream] in
for await work in stream {
try? await work()
}
}
}
func enqueue(_ work: @escaping @Sendable () async throws -> Void) async throws {
try await withCheckedThrowingContinuation { (done: CheckedContinuation<Void, Error>) in
continuation.yield {
do {
try await work()
done.resume()
} catch {
done.resume(throwing: error)
}
}
}
}
}
================================================
FILE: Sources/LLVS/Core/Value.swift
================================================
//
// Value.swift
// LLVS
//
// Created by Drew McCormack on 31/10/2018.
//
import Foundation
public struct Value: Codable, Identifiable {
public typealias ID = Identifier
public var id: ID
public var data: Data
/// The identifier of the version in which this value was stored. Can be nil, if
/// a value has not yet been stored.
public internal(set) var storedVersionId: Version.ID?
internal var zoneReference: ZoneReference? {
guard let version = storedVersionId else { return nil }
return ZoneReference(key: id.rawValue, version: version)
}
public var reference: Reference? {
guard let version = storedVersionId else { return nil }
return Reference(valueId: id, storedVersionId: version)
}
/// Convenience that saves creating IDs
public init(idString: String, data: Data) {
self.init(id: ID(idString), data: data)
}
/// If an id is not provided, a UUID will be used. The storedVersionId will be set to nil, because
/// this value has not been stored yet.
public init(id: ID = .init(UUID().uuidString), data: Data) {
self.id = id
self.data = data
}
internal init(id: ID, storedVersionId: Version.ID, data: Data) {
self.id = id
self.storedVersionId = storedVersionId
self.data = data
}
}
public extension Value {
struct Reference: Codable, Hashable {
public var valueId: ID
public var storedVersionId: Version.ID
}
struct Identifier: RawRepresentable, Hashable, Codable {
public var rawValue: String
public init(rawValue: String = UUID().uuidString) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
}
enum Change: Codable {
case insert(Value)
case update(Value)
case remove(ID)
case preserve(Reference)
case preserveRemoval(ID)
enum CodingKeys: String, CodingKey {
case insert, update, remove, preserve, preserveRemoval
}
enum Error: Swift.Error {
case changeDecodingFailure
}
public func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .insert(value):
try c.encode(value, forKey: .insert)
case let .update(value):
try c.encode(value, forKey: .update)
case let .remove(id):
try c.encode(id, forKey: .remove)
case let .preserve(ref):
try c.encode(ref, forKey: .preserve)
case let .preserveRemoval(id):
try c.encode(id, forKey: .preserveRemoval)
}
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
if let v = try? c.decode(Value.self, forKey: .insert) {
self = .insert(v); return
}
if let v = try? c.decode(Value.self, forKey: .update) {
self = .update(v); return
}
if let i = try? c.decode(ID.self, forKey: .remove) {
self = .remove(i); return
}
if let r = try? c.decode(Reference.self, forKey: .preserve) {
self = .preserve(r); return
}
if let i = try? c.decode(ID.self, forKey: .preserveRemoval) {
self = .preserveRemoval(i); return
}
throw Error.changeDecodingFailure
}
}
enum Fork: Equatable {
public enum Branch: Equatable {
case first
case second
var opposite: Branch {
return self == .first ? .second : .first
}
}
case inserted(Branch)
case twiceInserted
case removed(Branch)
case twiceRemoved
case updated(Branch)
case twiceUpdated
case removedAndUpdated(removedOn: Branch)
public var isConflicting: Bool {
switch self {
case .inserted, .removed, .updated, .twiceRemoved:
return false
case .twiceInserted, .twiceUpdated, .removedAndUpdated:
return true
}
}
}
}
extension Array where Element == Value.Change {
var valueIds: [Value.ID] {
return self.map { change in
switch change {
case .insert(let value), .update(let value):
return value.id
case .remove(let identifier), .preserveRemoval(let identifier):
return identifier
case .preserve(let ref):
return ref.valueId
}
}
}
var valueDataSize: Int64 {
return self.reduce(0) { result, change in
switch change {
case .insert(let value), .update(let value):
return result + Int64(value.data.count)
case .remove, .preserveRemoval, .preserve:
return result
}
}
}
}
================================================
FILE: Sources/LLVS/Core/Version.swift
================================================
//
// Version.swift
// llvs
//
// Created by Drew McCormack on 31/10/2018.
//
import Foundation
public struct Version: Hashable, Identifiable {
public typealias Metadata = [MetadataKey:MetadataValue]
public struct MetadataKey: Hashable, Codable, RawRepresentable {
public var rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public static let branch = Self(rawValue: "__llvs_branch")
}
public struct MetadataValue: Codable {
public let data: Data
public init(data: Data) { self.data = data }
public init<T: Codable>(_ value: T) { self.data = try! JSONEncoder().encode(value) }
public func value<T: Codable>() -> T { try! JSONDecoder().decode(T.self, from: self.data) }
}
public typealias ID = Identifier
public var id: ID = .init()
public var predecessors: Predecessors?
public var successors: Successors = .init()
public var timestamp: TimeInterval
public var valueDataSize: Int64?
public var metadata: Metadata = [:]
private enum CodingKeys: String, CodingKey {
case identifier
case predecessors
case timestamp
case valueDataSize
case metadata
}
public init(id: ID = .init(), predecessors: Predecessors? = nil, valueDataSize: Int64, metadata: Metadata = [:]) {
self.id = id
self.predecessors = predecessors
self.timestamp = Date().timeIntervalSinceReferenceDate
self.metadata = metadata
self.valueDataSize = valueDataSize
}
public static func == (lhs: Version, rhs: Version) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Version: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(ID.self, forKey: .identifier)
predecessors = try container.decodeIfPresent(Predecessors.self, forKey: .predecessors)
timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)
metadata = try container.decodeIfPresent(Metadata.self, forKey: .metadata) ?? [:]
valueDataSize = try container.decodeIfPresent(Int64.self, forKey: .valueDataSize)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .identifier)
try container.encodeIfPresent(predecessors, forKey: .predecessors)
try container.encode(timestamp, forKey: .timestamp)
try container.encodeIfPresent(valueDataSize, forKey: .valueDataSize)
try container.encode(metadata, forKey: .metadata)
}
}
extension Version {
public struct Identifier: RawRepresentable, Codable, Hashable {
public var rawValue: String
public init(rawValue: String = UUID().uuidString) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
}
public struct Predecessors: Codable, Hashable {
public internal(set) var idOfFirst: ID
public internal(set) var idOfSecond: ID?
public var ids: [ID] {
var result = [idOfFirst]
if let second = idOfSecond { result.append(second) }
return result
}
internal init(idOfFirst: ID, idOfSecond: ID?) {
self.idOfFirst = idOfFirst
self.idOfSecond = idOfSecond
}
}
public struct Successors: Codable, Hashable {
public internal(set) var ids: Set<ID>
internal init(ids: Set<ID> = []) {
self.ids = ids
}
}
}
public extension Collection where Element == Version {
var ids: [Version.ID] {
return map { $0.id }
}
var idStrings: [String] {
return map { $0.id.rawValue }
}
}
public extension Collection where Element == Version.ID {
var idStrings: [String] {
return map { $0.rawValue }
}
}
================================================
FILE: Sources/LLVS/Core/Zone.swift
================================================
//
// Zone.swift
// LLVS
//
// Created by Drew McCormack on 02/12/2018.
//
import Foundation
public struct ZoneReference: Codable, Hashable {
public var key: String
public var version: Version.ID
}
public protocol Zone {
func store(_ data: Data, for reference: ZoneReference) throws
// Default provided, but zone implementations can optimize this.
func store(_ data: [Data], for references: [ZoneReference]) throws
func data(for reference: ZoneReference) throws -> Data?
// Default provided, but zone implementations can optimize this.
func data(for references: [ZoneReference]) throws -> [Data?]
}
public extension Zone {
func store(_ data: [Data], for references: [ZoneReference]) throws {
try zip(data, references).forEach { data, ref in
try store(data, for: ref)
}
}
func data(for references: [ZoneReference]) throws -> [Data?] {
return try references.map { try data(for: $0) }
}
}
================================================
FILE: Sources/LLVS/Exchanges/CloudFileSystem.swift
================================================
//
// CloudFileSystem.swift
// LLVS
//
// Created by Drew McCormack on 03/03/2026.
//
import Foundation
/// Errors that can occur when interacting with a cloud file system.
public enum CloudFileSystemError: Error {
case fileNotFound
case uploadFailed
case downloadFailed
case directoryListingFailed
case authenticationFailed
case serverError(statusCode: Int)
}
/// A protocol for file-level operations on a cloud storage service.
///
/// Paths are relative strings (e.g. `"versions/ABC-123"`).
/// Implementations map these to service-specific addressing.
/// `upload` creates intermediate directories as needed.
public protocol CloudFileSystem: AnyObject, Sendable {
/// Returns `true` if a file exists at the given path.
func fileExists(at path: String) async throws -> Bool
/// Returns the file names (not full paths) of items in the directory at the given path.
func contentsOfDirectory(at path: String) async throws -> [String]
/// Uploads data to the given path, creating intermediate directories as needed.
func upload(data: Data, to path: String) async throws
/// Downloads data from the given path.
func download(from path: String) async throws -> Data
/// Removes the file at the given path. Does not throw if the file doesn't exist.
func remove(at path: String) async throws
/// Removes the directory at the given path and all its contents. Does not throw if the directory doesn't exist.
func removeDirectory(at path: String) async throws
}
================================================
FILE: Sources/LLVS/Exchanges/CloudFileSystemExchange.swift
================================================
//
// CloudFileSystemExchange.swift
// LLVS
//
// Created by Drew McCormack on 03/03/2026.
//
import Foundation
/// An exchange that stores version and change data in a cloud file system.
///
/// Implements `Exchange` and `SnapshotExchange` by delegating all I/O to a `CloudFileSystem`.
/// The on-disk layout mirrors `FileSystemExchange`:
///
/// ```
/// {basePath}/
/// versions/{versionId} -- JSON: {"version": Version}
/// changes/{versionId} -- JSON: [Value.Change]
/// snapshots/manifest.json -- JSON: SnapshotManifest
/// snapshots/chunk-000... -- Binary chunk data
/// ```
public final class CloudFileSystemExchange: Exchange, SnapshotExchange, @unchecked Sendable {
public enum Error: Swift.Error {
case versionFileInvalid
case changesFileInvalid
case snapshotChunkMissing(Int)
}
public let store: Store
public let cloudFileSystem: CloudFileSystem
/// A path prefix allowing multiple stores per cloud account.
public let basePath: String
public let newVersionsAvailable: AsyncStream<Void>
private let newVersionsContinuation: AsyncStream<Void>.Continuation
public var restorationState: Data? {
get { return nil }
set {}
}
private var versionsPath: String { basePath + "/versions" }
private var changesPath: String { basePath + "/changes" }
private var snapshotsPath: String { basePath + "/snapshots" }
/// Creates a cloud file system exchange.
/// - Parameters:
/// - cloudFileSystem: The cloud file system to use for I/O.
/// - store: The local LLVS store.
/// - basePath: A path prefix for all cloud files (default: empty string).
public init(cloudFileSystem: CloudFileSystem, store: Store, basePath: String = "") {
self.cloudFileSystem = cloudFileSystem
self.store = store
self.basePath = basePath
(self.newVersionsAvailable, self.newVersionsContinuation) = AsyncStream<Void>.makeStream()
}
deinit {
newVersionsContinuation.finish()
}
// MARK: - Exchange
public func prepareToRetrieve() async throws {
}
public func retrieveAllVersionIdentifiers() async throws -> [Version.ID] {
do {
let names = try await cloudFileSystem.contentsOfDirectory(at: versionsPath)
return names.map { Version.ID($0) }
} catch let error as CloudFileSystemError where error.isNotFound {
return []
}
}
public func retrieveVersions(identifiedBy versionIds: [Version.ID]) async throws -> [Version] {
try await versionIds.asyncMap { versionId in
let path = self.versionsPath + "/\(versionId.rawValue)"
let data = try await self.cloudFileSystem.download(from: path)
if let version = try JSONDecoder().decode([String: Version].self, from: data)["version"] {
return version
} else {
throw Error.versionFileInvalid
}
}
}
public func retrieveValueChanges(forVersionsIdentifiedBy versionIds: [Version.ID]) async throws -> [Version.ID: [Value.Change]] {
var result: [Version.ID: [Value.Change]] = [:]
for versionId in versionIds {
let path = changesPath + "/\(versionId.rawValue)"
let data = try await cloudFileSystem.download(from: path)
let changes = try JSONDecoder().decode([Value.Change].self, from: data)
result[versionId] = changes
}
return result
}
public func prepareToSend() async throws {
}
public func send(versionChanges: [VersionChanges]) async throws {
for (version, valueChanges) in versionChanges {
// Upload changes before version file for consistency
let changesData = try JSONEncoder().encode(valueChanges)
try await cloudFileSystem.upload(data: changesData, to: changesPath + "/\(version.id.rawValue)")
let versionData = try JSONEncoder().encode(["version": version])
try await cloudFileSystem.upload(data: versionData, to: versionsPath + "/\(version.id.rawValue)")
}
}
// MARK: - Snapshot Exchange
public func retrieveSnapshotManifest() async throws -> SnapshotManifest? {
let manifestPath = snapshotsPath + "/manifest.json"
do {
let exists = try await cloudFileSystem.fileExists(at: manifestPath)
guard exists else { return nil }
let data = try await cloudFileSystem.download(from: manifestPath)
return try JSONDecoder().decode(SnapshotManifest.self, from: data)
} catch let error as CloudFileSystemError where error.isNotFound {
return nil
}
}
public func retrieveSnapshotChunk(index: Int) async throws -> Data {
let chunkPath = snapshotsPath + "/" + String(format: "chunk-%03d", index)
do {
return try await cloudFileSystem.download(from: chunkPath)
} catch {
throw Error.snapshotChunkMissing(index)
}
}
public func sendSnapshot(manifest: SnapshotManifest, chunkProvider: @escaping @Sendable (Int) throws -> Data) async throws {
// Remove previous snapshot if any
try? await cloudFileSystem.removeDirectory(at: snapshotsPath)
// Write chunks first
for i in 0..<manifest.chunkCount {
let chunkData = try chunkProvider(i)
let chunkPath = snapshotsPath + "/" + String(format: "chunk-%03d", i)
try await cloudFileSystem.upload(data: chunkData, to: chunkPath)
}
// Write manifest last
let manifestData = try JSONEncoder().encode(manifest)
try await cloudFileSystem.upload(data: manifestData, to: snapshotsPath + "/manifest.json")
}
}
// MARK: - Async Helpers
private extension Array {
func asyncMap<T>(_ transform: @escaping (Element) async throws -> T) async throws -> [T] {
var results: [T] = []
results.reserveCapacity(count)
for element in self {
try await results.append(transform(element))
}
return results
}
}
// MARK: - CloudFileSystemError Helpers
extension CloudFileSystemError {
var isNotFound: Bool {
if case .fileNotFound = self { return true }
if case .directoryListingFailed = self { return true }
return false
}
}
================================================
FILE: Sources/LLVS/Exchanges/FileSystemExchange.swift
================================================
//
// FileSystemExchange.swift
// LLVS
//
// Created by Drew McCormack on 25/02/2019.
//
import Foundation
public class FileSystemExchange: NSObject, Exchange, NSFilePresenter, SnapshotExchange {
public enum Error: Swift.Error {
case versionFileInvalid
case changesFileInvalid
case snapshotChunkMissing(Int)
}
public let store: Store
private let minimumDelayBeforeNotifyingOfNewVersions = 1.0
public let newVersionsAvailable: AsyncStream<Void>
private let newVersionsContinuation: AsyncStream<Void>.Continuation
public let rootDirectoryURL: URL
public var versionsDirectory: URL { return rootDirectoryURL.appendingPathComponent("versions") }
public var changesDirectory: URL { return rootDirectoryURL.appendingPathComponent("changes") }
public var snapshotsDirectory: URL { return rootDirectoryURL.appendingPathComponent("snapshots") }
public let usesFileCoordination: Bool
public var restorationState: Data? {
get { return nil }
set {}
}
fileprivate let fileManager = FileManager()
fileprivate let queue = OperationQueue()
public init(rootDirectoryURL: URL, store: Store, usesFileCoordination: Bool) {
self.rootDirectoryURL = rootDirectoryURL
self.store = store
self.usesFileCoordination = usesFileCoordination
(self.newVersionsAvailable, self.newVersionsContinuation) = AsyncStream<Void>.makeStream()
super.init()
try? fileManager.createDirectory(at: rootDirectoryURL, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: versionsDirectory, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: changesDirectory, withIntermediateDirectories: true, attributes: nil)
if self.usesFileCoordination {
NSFileCoordinator.addFilePresenter(self)
}
}
deinit {
if self.usesFileCoordination {
NSFileCoordinator.
gitextract_ytf8b50v/
├── .gitignore
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENCE.txt
├── Package.swift
├── README.md
├── Samples/
│ ├── LoCo/
│ │ ├── LoCo/
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AccentColor.colorset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── Contact.swift
│ │ │ ├── ContactStore.swift
│ │ │ ├── ContactView.swift
│ │ │ ├── ContactsView.swift
│ │ │ ├── LoCo.entitlements
│ │ │ ├── LoCoApp.swift
│ │ │ └── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ └── LoCo.xcodeproj/
│ │ └── project.pbxproj
│ └── TheMessage/
│ ├── TheMessage/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── ContentView.swift
│ │ ├── MessageStore.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ ├── TheMessage.entitlements
│ │ └── TheMessageApp.swift
│ └── TheMessage.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── Sources/
│ ├── LLVS/
│ │ ├── Core/
│ │ │ ├── Exchange.swift
│ │ │ ├── FileZone.swift
│ │ │ ├── FolderBasedExchange.swift
│ │ │ ├── History.swift
│ │ │ ├── Map.swift
│ │ │ ├── Merge.swift
│ │ │ ├── Snapshot.swift
│ │ │ ├── SnapshotCapable+ZIP.swift
│ │ │ ├── Storage.swift
│ │ │ ├── Store.swift
│ │ │ ├── StoreCoordinator.swift
│ │ │ ├── Value.swift
│ │ │ ├── Version.swift
│ │ │ └── Zone.swift
│ │ ├── Exchanges/
│ │ │ ├── CloudFileSystem.swift
│ │ │ ├── CloudFileSystemExchange.swift
│ │ │ ├── FileSystemExchange.swift
│ │ │ ├── MemoryExchange.swift
│ │ │ └── MultipeerExchange.swift
│ │ ├── General/
│ │ │ ├── Cache.swift
│ │ │ ├── DataCompression.swift
│ │ │ ├── DynamicTaskBatcher.swift
│ │ │ ├── General.swift
│ │ │ └── Log.swift
│ │ └── Utilities/
│ │ └── ArrayDiff.swift
│ ├── LLVSBox/
│ │ └── BoxExchange.swift
│ ├── LLVSCloudKit/
│ │ └── CloudKitExchange.swift
│ ├── LLVSGoogleDrive/
│ │ ├── GoogleDriveAuthenticator.swift
│ │ └── GoogleDriveFileSystem.swift
│ ├── LLVSModel/
│ │ ├── Macros.swift
│ │ ├── Mergeable.swift
│ │ ├── MergeableArbiter.swift
│ │ ├── StorableModel.swift
│ │ └── StoreCoordinator+Model.swift
│ ├── LLVSModelMacros/
│ │ ├── MergeableModelMacro.swift
│ │ └── Plugin.swift
│ ├── LLVSOneDrive/
│ │ ├── OneDriveAuthenticator.swift
│ │ └── OneDriveFileSystem.swift
│ ├── LLVSPCloud/
│ │ └── PCloudExchange.swift
│ ├── LLVSSQLite/
│ │ ├── SQLiteDatabase+Zones.swift
│ │ ├── SQLiteDatabase.swift
│ │ └── SQLiteZone.swift
│ ├── LLVSWebDAV/
│ │ ├── WebDAVFileSystem.swift
│ │ └── WebDAVResponseParser.swift
│ └── SQLite3/
│ ├── module.modulemap
│ └── shim.h
├── Tests/
│ ├── LLVSModelTests/
│ │ └── MergeableArbiterTests.swift
│ └── LLVSTests/
│ ├── ArrayDiffTests.swift
│ ├── CloudFileSystemExchangeTests.swift
│ ├── DataCompressionTests.swift
│ ├── DiffTests.swift
│ ├── DynamicTaskBatcherTests.swift
│ ├── FileSystemExchangeTests.swift
│ ├── FileZoneTests.swift
│ ├── GeneralTests.swift
│ ├── HistoryTests.swift
│ ├── MapTests.swift
│ ├── MemoryExchangeTests.swift
│ ├── MergeTests.swift
│ ├── MostRecentBranchArbiterTests.swift
│ ├── MostRecentChangeArbiterTests.swift
│ ├── MultipeerExchangeTests.swift
│ ├── PerformanceTests.swift
│ ├── PrevailingValueTests.swift
│ ├── SQLitePerformanceTests.swift
│ ├── SQLiteZoneTests.swift
│ ├── SerialHistoryTests.swift
│ ├── SharedStoreTests.swift
│ ├── SnapshotTests.swift
│ ├── StoreSetupTests.swift
│ ├── ValueChangesInVersionTests.swift
│ ├── ValueTests.swift
│ └── VersionTests.swift
└── docs/
├── _config.yml
├── _posts/
│ └── 2019-09-25-data-driven-swiftui.md
└── index.md
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (575K chars).
[
{
"path": ".gitignore",
"chars": 245,
"preview": ".DS_Store\n/.build\n/.swiftpm\n**/Package.resolved\n/Packages\n\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2"
},
{
"path": "CLAUDE.md",
"chars": 5656,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "CONTRIBUTING.md",
"chars": 6227,
"preview": "### Low-Level Versioned Store (LLVS) – Contributor License Agreement (v1)\n\nThank you for your interest in the Low-Level "
},
{
"path": "LICENCE.txt",
"chars": 1077,
"preview": " Copyright (c) 2019 Drew McCormack\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of th"
},
{
"path": "Package.swift",
"chars": 3937,
"preview": "// swift-tools-version: 6.1\n// The swift-tools-version declares the minimum version of Swift required to build this pack"
},
{
"path": "README.md",
"chars": 14906,
"preview": "[ var store\n @State private var draft: S"
},
{
"path": "Samples/TheMessage/TheMessage/MessageStore.swift",
"chars": 2052,
"preview": "import Foundation\nimport LLVS\nimport LLVSCloudKit\nimport CloudKit\n\n@MainActor @Observable\nclass MessageStore {\n var m"
},
{
"path": "Samples/TheMessage/TheMessage/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "Samples/TheMessage/TheMessage/TheMessage.entitlements",
"chars": 476,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Samples/TheMessage/TheMessage/TheMessageApp.swift",
"chars": 226,
"preview": "import SwiftUI\n\n@main\nstruct TheMessageApp: App {\n @State private var store = MessageStore()\n\n var body: some Scen"
},
{
"path": "Samples/TheMessage/TheMessage.xcodeproj/project.pbxproj",
"chars": 15317,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 155,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:TheMessage.xcod"
},
{
"path": "Samples/TheMessage/TheMessage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Sources/LLVS/Core/Exchange.swift",
"chars": 8236,
"preview": "//\n// Exchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 25/02/2019.\n//\n\nimport Foundation\n\nenum ExchangeError"
},
{
"path": "Sources/LLVS/Core/FileZone.swift",
"chars": 3903,
"preview": "//\n// FileZone.swift\n// LLVS\n//\n// Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\n\npublic class FileS"
},
{
"path": "Sources/LLVS/Core/FolderBasedExchange.swift",
"chars": 3464,
"preview": "//\n// FolderBasedExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\npublic "
},
{
"path": "Sources/LLVS/Core/History.swift",
"chars": 8063,
"preview": "//\n// History.swift\n// LLVS\n//\n// Created by Drew McCormack on 11/11/2018.\n//\n\nimport Foundation\n\npublic class Histor"
},
{
"path": "Sources/LLVS/Core/Map.swift",
"chars": 18982,
"preview": "//\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 "
},
{
"path": "Sources/LLVS/Core/Merge.swift",
"chars": 3544,
"preview": "//\n// Merge.swift\n// LLVS\n//\n// Created by Drew McCormack on 19/12/2018.\n//\n\nimport Foundation\n\npublic struct Merge {"
},
{
"path": "Sources/LLVS/Core/Snapshot.swift",
"chars": 1571,
"preview": "//\n// Snapshot.swift\n// LLVS\n//\n// Created by Drew McCormack on 09/02/2026.\n//\n\nimport Foundation\n\n/// Metadata descr"
},
{
"path": "Sources/LLVS/Core/SnapshotCapable+ZIP.swift",
"chars": 3696,
"preview": "//\n// SnapshotCapable+ZIP.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport Z"
},
{
"path": "Sources/LLVS/Core/Storage.swift",
"chars": 804,
"preview": "//\n// Storage.swift\n// LLVS\n//\n// Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\n\npublic enum MapType"
},
{
"path": "Sources/LLVS/Core/Store.swift",
"chars": 28212,
"preview": "//\n// Store.swift\n// llvs\n//\n// Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\nimport Synchronization"
},
{
"path": "Sources/LLVS/Core/StoreCoordinator.swift",
"chars": 15157,
"preview": "//\n// StoreCoordinator.swift\n// LLVS\n//\n// Created by Drew McCormack on 09/06/2019.\n// Copyright © 2019 Momenta B.V."
},
{
"path": "Sources/LLVS/Core/Value.swift",
"chars": 5306,
"preview": "//\n// Value.swift\n// LLVS\n//\n// Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\n\npublic struct Value: "
},
{
"path": "Sources/LLVS/Core/Version.swift",
"chars": 4216,
"preview": "//\n// Version.swift\n// llvs\n//\n// Created by Drew McCormack on 31/10/2018.\n//\n\nimport Foundation\n\npublic struct Versi"
},
{
"path": "Sources/LLVS/Core/Zone.swift",
"chars": 986,
"preview": "//\n// Zone.swift\n// LLVS\n//\n// Created by Drew McCormack on 02/12/2018.\n//\n\nimport Foundation\n\npublic struct ZoneRefe"
},
{
"path": "Sources/LLVS/Exchanges/CloudFileSystem.swift",
"chars": 1536,
"preview": "//\n// CloudFileSystem.swift\n// LLVS\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\n\n/// Errors "
},
{
"path": "Sources/LLVS/Exchanges/CloudFileSystemExchange.swift",
"chars": 6404,
"preview": "//\n// CloudFileSystemExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\n\n///"
},
{
"path": "Sources/LLVS/Exchanges/FileSystemExchange.swift",
"chars": 9483,
"preview": "//\n// FileSystemExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 25/02/2019.\n//\n\nimport Foundation\n\npublic c"
},
{
"path": "Sources/LLVS/Exchanges/MemoryExchange.swift",
"chars": 2067,
"preview": "//\n// MemoryExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\n\npublic actor"
},
{
"path": "Sources/LLVS/Exchanges/MultipeerExchange.swift",
"chars": 10479,
"preview": "//\n// MultipeerExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\n/// Proto"
},
{
"path": "Sources/LLVS/General/Cache.swift",
"chars": 2912,
"preview": "//\n// Cache.swift\n// LLVS\n//\n// Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\nimport Synchronization"
},
{
"path": "Sources/LLVS/General/DataCompression.swift",
"chars": 3269,
"preview": "//\n// DataCompression.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\n\n/// Transpa"
},
{
"path": "Sources/LLVS/General/DynamicTaskBatcher.swift",
"chars": 4162,
"preview": "//\n// DynamicTaskBatcher.swift\n//\n//\n// Created by Drew McCormack on 06/03/2020.\n//\n\nimport Foundation\n\n/// Generates "
},
{
"path": "Sources/LLVS/General/General.swift",
"chars": 1446,
"preview": "//\n// General.swift\n// LLVS\n//\n// Created by Drew McCormack on 04/11/2018.\n//\n\nimport Foundation\nimport Synchronizati"
},
{
"path": "Sources/LLVS/General/Log.swift",
"chars": 2490,
"preview": "//\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 "
},
{
"path": "Sources/LLVS/Utilities/ArrayDiff.swift",
"chars": 13944,
"preview": "//\n// ArrayMerge.swift\n// LLVS\n//\n// Created by Drew McCormack on 02/04/2019.\n//\n\nimport Foundation\n\npublic extension"
},
{
"path": "Sources/LLVSBox/BoxExchange.swift",
"chars": 6612,
"preview": "//\n// BoxExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\nimport LLVS\nimpo"
},
{
"path": "Sources/LLVSCloudKit/CloudKitExchange.swift",
"chars": 35178,
"preview": "//\n// CloudKitExchange\n// LLVS\n//\n// Created by Drew McCormack on 16/03/2019.\n//\n\nimport Foundation\nimport CloudKit\ni"
},
{
"path": "Sources/LLVSGoogleDrive/GoogleDriveAuthenticator.swift",
"chars": 10837,
"preview": "//\n// GoogleDriveAuthenticator.swift\n// LLVSGoogleDrive\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Fou"
},
{
"path": "Sources/LLVSGoogleDrive/GoogleDriveFileSystem.swift",
"chars": 18212,
"preview": "//\n// GoogleDriveFileSystem.swift\n// LLVSGoogleDrive\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Founda"
},
{
"path": "Sources/LLVSModel/Macros.swift",
"chars": 574,
"preview": "//\n// Macros.swift\n// LLVS\n//\n// Created by Drew McCormack on 04/03/2026.\n//\n\n/// Generates a `Mergeable` conformance"
},
{
"path": "Sources/LLVSModel/Mergeable.swift",
"chars": 2290,
"preview": "//\n// Mergeable.swift\n// LLVS\n//\n// Created by Drew McCormack on 04/03/2026.\n//\n\nimport Foundation\n\n/// A type that s"
},
{
"path": "Sources/LLVSModel/MergeableArbiter.swift",
"chars": 4544,
"preview": "//\n// MergeableArbiter.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport LLVS"
},
{
"path": "Sources/LLVSModel/StorableModel.swift",
"chars": 1537,
"preview": "//\n// StorableModel.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimport LLVS\n\n/"
},
{
"path": "Sources/LLVSModel/StoreCoordinator+Model.swift",
"chars": 2177,
"preview": "//\n// StoreCoordinator+Model.swift\n// LLVS\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Foundation\nimpor"
},
{
"path": "Sources/LLVSModelMacros/MergeableModelMacro.swift",
"chars": 3576,
"preview": "//\n// MergeableModelMacro.swift\n// LLVS\n//\n// Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftSyntax\nimport "
},
{
"path": "Sources/LLVSModelMacros/Plugin.swift",
"chars": 270,
"preview": "//\n// Plugin.swift\n// LLVS\n//\n// Created by Drew McCormack on 04/03/2026.\n//\n\nimport SwiftCompilerPlugin\nimport Swift"
},
{
"path": "Sources/LLVSOneDrive/OneDriveAuthenticator.swift",
"chars": 11338,
"preview": "//\n// OneDriveAuthenticator.swift\n// LLVSOneDrive\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundatio"
},
{
"path": "Sources/LLVSOneDrive/OneDriveFileSystem.swift",
"chars": 9689,
"preview": "//\n// OneDriveFileSystem.swift\n// LLVSOneDrive\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\ni"
},
{
"path": "Sources/LLVSPCloud/PCloudExchange.swift",
"chars": 7562,
"preview": "//\n// PCloudExchange.swift\n// LLVS\n//\n// Created by Drew McCormack on 28/02/2026.\n//\n\nimport Foundation\nimport LLVS\ni"
},
{
"path": "Sources/LLVSSQLite/SQLiteDatabase+Zones.swift",
"chars": 3555,
"preview": "//\n// SQLiteDatabase+Zones.swift\n// \n//\n// Created by Drew McCormack on 16/02/2022.\n//\n\nimport Foundation\nimport LLVS"
},
{
"path": "Sources/LLVSSQLite/SQLiteDatabase.swift",
"chars": 8412,
"preview": "//\n// Database.swift\n// LLVS\n//\n// Created by Drew McCormack on 19/01/2017.\n//\n\nimport Foundation\nimport SQLite3\n\n// "
},
{
"path": "Sources/LLVSSQLite/SQLiteZone.swift",
"chars": 3828,
"preview": "//\n// SQLiteZone.swift\n// LLVS\n//\n// Created by Drew McCormack on 14/05/2019.\n//\n\nimport Foundation\nimport LLVS\nimpor"
},
{
"path": "Sources/LLVSWebDAV/WebDAVFileSystem.swift",
"chars": 8840,
"preview": "//\n// WebDAVFileSystem.swift\n// LLVSWebDAV\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\nimpor"
},
{
"path": "Sources/LLVSWebDAV/WebDAVResponseParser.swift",
"chars": 2710,
"preview": "//\n// WebDAVResponseParser.swift\n// LLVSWebDAV\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Foundation\ni"
},
{
"path": "Sources/SQLite3/module.modulemap",
"chars": 70,
"preview": "module SQLite {\n header \"shim.h\"\n link \"sqlite3\"\n export *\n}\n"
},
{
"path": "Sources/SQLite3/shim.h",
"chars": 21,
"preview": "#include <sqlite3.h>\n"
},
{
"path": "Tests/LLVSModelTests/MergeableArbiterTests.swift",
"chars": 13149,
"preview": "//\n// MergeableArbiterTests.swift\n// LLVSModelTests\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing"
},
{
"path": "Tests/LLVSTests/ArrayDiffTests.swift",
"chars": 7019,
"preview": "//\n// ArrayDiffTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 02/04/2019.\n//\n\nimport Testing\n@testable i"
},
{
"path": "Tests/LLVSTests/CloudFileSystemExchangeTests.swift",
"chars": 8336,
"preview": "//\n// CloudFileSystemExchangeTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 03/03/2026.\n//\n\nimport Testi"
},
{
"path": "Tests/LLVSTests/DataCompressionTests.swift",
"chars": 4183,
"preview": "//\n// DataCompressionTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimpor"
},
{
"path": "Tests/LLVSTests/DiffTests.swift",
"chars": 7237,
"preview": "//\n// DiffTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 11/01/2019.\n//\n\nimport Testing\nimport Foundatio"
},
{
"path": "Tests/LLVSTests/DynamicTaskBatcherTests.swift",
"chars": 4355,
"preview": "//\n// DynamicTaskBatcherTests.swift\n//\n//\n// Created by Drew McCormack on 06/03/2020.\n//\n\nimport Foundation\n\nimport Te"
},
{
"path": "Tests/LLVSTests/FileSystemExchangeTests.swift",
"chars": 6344,
"preview": "//\n// FileSystemExchangeTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 08/03/2019.\n//\n\nimport Testing\nim"
},
{
"path": "Tests/LLVSTests/FileZoneTests.swift",
"chars": 2303,
"preview": "//\n// FileZoneTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 07/12/2018.\n//\n\nimport Testing\nimport Found"
},
{
"path": "Tests/LLVSTests/GeneralTests.swift",
"chars": 704,
"preview": "//\n// File.swift\n//\n//\n// Created by Drew McCormack on 15/09/2019.\n//\n\nimport Foundation\nimport Testing\n@testable impo"
},
{
"path": "Tests/LLVSTests/HistoryTests.swift",
"chars": 7084,
"preview": "//\n// HistoryTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 12/11/2018.\n//\n\nimport Testing\n@testable imp"
},
{
"path": "Tests/LLVSTests/MapTests.swift",
"chars": 5811,
"preview": "//\n// MapTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 09/12/2018.\n//\n\nimport Testing\nimport Foundation"
},
{
"path": "Tests/LLVSTests/MemoryExchangeTests.swift",
"chars": 5985,
"preview": "//\n// MemoryExchangeTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimport"
},
{
"path": "Tests/LLVSTests/MergeTests.swift",
"chars": 9105,
"preview": "//\n// MergeTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 12/01/2019.\n//\n\nimport Testing\nimport Foundati"
},
{
"path": "Tests/LLVSTests/MostRecentBranchArbiterTests.swift",
"chars": 6214,
"preview": "//\n// MergeArbiterTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 09/03/2019.\n//\n\nimport Testing\nimport F"
},
{
"path": "Tests/LLVSTests/MostRecentChangeArbiterTests.swift",
"chars": 7519,
"preview": "//\n// MostRecentChangeFavoringArbiterTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 10/03/2019.\n//\n\nimpo"
},
{
"path": "Tests/LLVSTests/MultipeerExchangeTests.swift",
"chars": 7407,
"preview": "//\n// MultipeerExchangeTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 01/03/2026.\n//\n\nimport Testing\nimp"
},
{
"path": "Tests/LLVSTests/PerformanceTests.swift",
"chars": 1712,
"preview": "//\n// PerformanceTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 14/02/2019.\n//\n\nimport Testing\nimport Fo"
},
{
"path": "Tests/LLVSTests/PrevailingValueTests.swift",
"chars": 2018,
"preview": "//\n// PrevailingValueTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 23/11/2018.\n//\n\nimport Testing\nimpor"
},
{
"path": "Tests/LLVSTests/SQLitePerformanceTests.swift",
"chars": 1808,
"preview": "//\n// SQLitePerformanceTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 16/02/2022.\n//\n\nimport Testing\nimp"
},
{
"path": "Tests/LLVSTests/SQLiteZoneTests.swift",
"chars": 2079,
"preview": "//\n// SQLiteZoneTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 16/02/2022.\n//\n\nimport Testing\nimport Fou"
},
{
"path": "Tests/LLVSTests/SerialHistoryTests.swift",
"chars": 2710,
"preview": "//\n// SerialHistoryTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 04/02/2019.\n//\n\nimport Testing\nimport "
},
{
"path": "Tests/LLVSTests/SharedStoreTests.swift",
"chars": 1887,
"preview": "//\n// SharedStoreTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 16/03/2019.\n//\n\nimport Testing\nimport Fo"
},
{
"path": "Tests/LLVSTests/SnapshotTests.swift",
"chars": 30652,
"preview": "//\n// SnapshotTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 09/02/2026.\n//\n\nimport Testing\nimport Found"
},
{
"path": "Tests/LLVSTests/StoreSetupTests.swift",
"chars": 814,
"preview": "import Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class StoreSetupTests {\n\n let store: Store\n let roo"
},
{
"path": "Tests/LLVSTests/ValueChangesInVersionTests.swift",
"chars": 1508,
"preview": "//\n// ValueChangesInVersionTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 13/03/2019.\n//\n\nimport Testing"
},
{
"path": "Tests/LLVSTests/ValueTests.swift",
"chars": 3046,
"preview": "import Testing\nimport Foundation\n@testable import LLVS\n\n@Suite class ValueTests {\n\n let fm = FileManager.default\n\n "
},
{
"path": "Tests/LLVSTests/VersionTests.swift",
"chars": 1217,
"preview": "//\n// VersionTests.swift\n// LLVSTests\n//\n// Created by Drew McCormack on 26/01/2019.\n//\n\nimport Testing\nimport Founda"
},
{
"path": "docs/_config.yml",
"chars": 329,
"preview": "description: >-\n Discussion of the Low-Level Versioned Store (LLVS) and\n other aspects of distributed data storage.\nba"
},
{
"path": "docs/_posts/2019-09-25-data-driven-swiftui.md",
"chars": 11602,
"preview": "---\nlayout: post\ntitle: \"Data Driven SwiftUI\"\nauthor: Drew McCormack\ntags: [swiftui]\n---\n\nSwiftUI has set us all thinkin"
},
{
"path": "docs/index.md",
"chars": 163,
"preview": "## Posts\n\n<ul>\n {% for post in site.posts %}\n <li>\n <a href=\"{{ post.url | prepend: site.baseurl }}\">{{ post.t"
}
]
About this extraction
This page contains the full source code of the mentalfaculty/LLVS GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (535.0 KB), approximately 129.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.