Migrate the sync file layer onto the IndieSync package

Replaces the app-local copies of the extracted storage core with the
IndieSync 0.1.0 package (pinned from the new tag): ICloudFileManager ->
DocumentFileStore + TombstoneStore, ICloudFileMonitor ->
MetadataObserver (batched events with the content-date churn gate),
and the shared ULID / DocumentCoder / Tombstone / VersionedDocument
pieces. SyncEngine stays app-specific and is rewired onto the package
types; documents rename currentSchema -> currentSchemaVersion to adopt
the package protocol. ULID.make() remains as a shim minting the string
form the app keys on.

Behavior-preserving: identical JSON bytes (same coder config), same
stub wire format (kind encodes as the same strings), same soft-delete
ordering, same 30-day prune. WorkoutDocument keeps its local-calendar
month bucketing rather than adopting TimeBucketedLayout's UTC paths --
changing derivation would strand existing files. The watch target
links the package too (ULID + DocumentCoder via Shared); iOS, watchOS,
and widget targets all build.
This commit is contained in:
2026-07-05 08:04:29 -04:00
parent 3e63adf363
commit 1f2df491db
18 changed files with 98 additions and 524 deletions
+8 -65
View File
@@ -1,68 +1,11 @@
import Foundation
import IndieSync
/// Minimal ULID generator.
///
/// 26-character Crockford base32 string: 10 chars of millisecond timestamp +
/// 16 chars of randomness. Lexicographic order matches chronological order,
/// so workout documents sort newest-last by id and file listings stay ordered.
///
/// Spec: https://github.com/ulid/spec
enum ULID {
/// Crockford base32 alphabet no I, L, O, U to avoid visual confusion.
private static let alphabet: [Character] = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
/// Mints a fresh ULID for the current instant.
static func make() -> String {
let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
var randomness = [UInt8](repeating: 0, count: 10)
_ = randomness.withUnsafeMutableBytes { buffer in
SecRandomCopyBytes(kSecRandomDefault, buffer.count, buffer.baseAddress!)
}
return encode(timestamp: timestamp, randomness: randomness)
}
/// True if `s` is a 26-char ULID using the Crockford alphabet.
static func isValid(_ s: String) -> Bool {
guard s.count == 26 else { return false }
let allowed = Set(alphabet)
return s.allSatisfy { allowed.contains($0) }
}
/// Decodes the timestamp portion of a ULID, if valid.
static func timestamp(of ulid: String) -> Date? {
guard ulid.count == 26 else { return nil }
let prefix = ulid.prefix(10)
var value: UInt64 = 0
let lookup = Dictionary(uniqueKeysWithValues: alphabet.enumerated().map { ($1, UInt64($0)) })
for char in prefix {
guard let digit = lookup[char] else { return nil }
value = (value << 5) | digit
}
return Date(timeIntervalSince1970: Double(value) / 1000)
}
// MARK: - Internal
private static func encode(timestamp: UInt64, randomness: [UInt8]) -> String {
precondition(randomness.count == 10, "ULID randomness must be 10 bytes (80 bits)")
// 48-bit timestamp 10 base32 chars (50 bits, top 2 unused).
var output = [Character](repeating: "0", count: 26)
var t = timestamp & ((1 << 48) - 1)
for i in stride(from: 9, through: 0, by: -1) {
output[i] = alphabet[Int(t & 0x1F)]
t >>= 5
}
// 80-bit randomness 16 base32 chars.
var bigEnd = randomness
for i in stride(from: 25, through: 10, by: -1) {
var carry: UInt16 = 0
for j in 0..<bigEnd.count {
let combined = UInt16(bigEnd[j]) | (carry << 8)
bigEnd[j] = UInt8(combined >> 5)
carry = combined & 0x1F
}
output[i] = alphabet[Int(UInt8(carry))]
}
return String(output)
}
// The ULID implementation lives in the IndieSync package. Workouts' documents
// and cache entities key on the 26-char string form, so this shim keeps the
// app-wide `ULID.make()` minting entry point unchanged.
extension ULID {
/// Mints a fresh ULID for the current instant, as its canonical
/// 26-character Crockford base32 string.
static func make() -> String { ULID().stringValue }
}