Acorn Core
The foundation of Lattice. Defines the content-addressable storage protocol, content identifiers, composite chaining, and the LFU eviction cache.
.package(url: "https://github.com/treehauslabs/Acorn.git", from: "1.0.0")
AcornCASWorker Protocol
The core protocol that all storage workers implement. Requires actor conformance for thread safety.
public protocol AcornCASWorker: Actor {
var timeout: Duration? { get }
var near: (any AcornCASWorker)? { get set }
var far: (any AcornCASWorker)? { get set }
func has(cid: ContentIdentifier) async -> Bool
func getLocal(cid: ContentIdentifier) async -> Data?
func storeLocal(cid: ContentIdentifier, data: Data) async
}
Required Methods
| Method | Description |
|---|---|
has(cid:) | Check if this worker has content locally. Must not traverse the chain. |
getLocal(cid:) | Retrieve data from this worker's local storage only. |
storeLocal(cid:data:) | Store data in this worker's local storage only. |
Protocol-Provided Methods
These are implemented via a protocol extension and handle chain traversal automatically:
| Method | Behavior |
|---|---|
get(cid:) |
Check near first → fall back to getLocal() → backfill toward near on success. Respects timeout. |
store(cid:data:) |
Call storeLocal() then propagate to near via store(). |
link(near:far:) |
Set the near and far references in one call. |
has, getLocal, and storeLocal. The protocol extension provides get, store, and link with full chain traversal and backfilling logic.
Timeout Support
The protocol includes a helper function for timeout enforcement:
func withOptionalTimeout<T: Sendable>(
_ duration: Duration?,
operation: @escaping @Sendable () async -> T?
) async -> T?
If duration is nil, the operation runs without a timeout. Otherwise, a task group races the operation against a sleep, cancelling whichever loses.
ContentIdentifier
A SHA-256 content address. Two identical byte sequences always produce the same identifier, regardless of where or when they're computed.
public struct ContentIdentifier: Hashable, Sendable {
public let rawValue: String // 64-character hex SHA-256
public init(for data: Data) // compute from data
public init(rawValue: String) // from known hash
}
Performance
ContentIdentifier caches the first 8 bytes of the hash as an Int for O(1) dictionary lookups. The full 64-character string comparison only happens on hash collisions.
let data = Data("Hello".utf8)
let cid = ContentIdentifier(for: data)
print(cid.rawValue) // "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"
// Deterministic: same data → same CID
let cid2 = ContentIdentifier(for: data)
assert(cid == cid2) // always true
CompositeCASWorker
An actor that chains multiple workers into a single storage hierarchy. The initializer automatically links workers by setting near/far references.
public actor CompositeCASWorker: AcornCASWorker {
public init(
workers: [String: any AcornCASWorker],
order: [String],
timeout: Duration? = nil
) async
public subscript(name: String) -> (any AcornCASWorker)?
}
Parameters
Chain Linking
Given order: ["memory", "disk", "network"], the initializer sets:
memory.far = disk,disk.near = memorydisk.far = network,network.near = disk
All get() and store() calls delegate to the farthest worker, which recursively walks toward near.
Subscript Access
let disk = chain["disk"] as? DiskCASWorker
let metrics = await disk?.metrics
LFUDecayCache
A Least-Frequently-Used cache with exponential time decay. Used internally by both MemoryCASWorker and DiskCASWorker for eviction decisions.
public struct LFUDecayCache: Sendable {
public init(
capacity: Int,
halfLife: Duration = .seconds(300),
sampleSize: Int = 5
)
}
How It Works
- Access scoring: Each
recordAccess(cid)increments the CID's score, adjusted by a global decay multiplier - Exponential decay: Scores decay with the configured half-life. A score from 5 minutes ago is worth half as much as a score from now (with default 300s half-life)
- Sampled eviction:
evictionCandidate()picks N random entries and returns the one with the lowest effective score. This is O(k) instead of O(n) - Global multiplier: Instead of decaying every score on every access, a single multiplier tracks aggregate decay. Individual scores are divided by this multiplier on read
- Renormalization: When the multiplier risks float underflow (~1e100), scores are batch-renormalized in the background
Key Methods
| Method | Description |
|---|---|
recordAccess(_:) | Increment score for a CID. Adds to tracking if new. |
evictionCandidate() | Return the lowest-scored CID among sampleSize random samples. |
needsEviction(for:) | True if cache is at capacity and CID isn't already tracked. |
effectiveScore(for:) | Current score accounting for time-based decay. |
remove(_:) | Remove a CID from tracking. O(1) via swap-remove. |
claimRenormalization() | If renormalization is pending, return the factor and key list. |