HBStorage
The main persistence API for saving, loading, and syncing spaces and anchors.
Overview
HBStorage is the primary interface for all persistence operations. It handles local file storage and optional cloud sync.
let storage = HBStorage()
// or
let storage = HBStorage(config: .default)Initialization
Local-only storage (default)
let storage = HBStorage()With cloud sync
let storage = HBStorage(config: HBStorageConfig(
backend: .supabase(
url: "https://xxx.supabase.co",
anonKey: "eyJ..."
),
syncStrategy: .onSave,
compression: .balanced
))Space Operations
Save a space
public func save(_ space: HBSpace) async throwslet worldMap = try await arSession.currentWorldMap
let space = try HBSpace(name: "Living Room", worldMap: worldMap)
try await storage.save(space)Load a space by ID
public func loadSpace(id: UUID) async throws -> HBSpace?if let space = try await storage.loadSpace(id: savedId) {
let worldMap = try space.arWorldMap()
// Use worldMap for relocalization
}Load all spaces
public func loadAllSpaces() async throws -> [HBSpace]let spaces = try await storage.loadAllSpaces()
let mostRecent = spaces.max(by: { $0.updatedAt < $1.updatedAt })Delete a space
public func deleteSpace(id: UUID) async throwsDeleting a space also deletes all anchors associated with it. This operation cannot be undone.
Anchor Operations
Save an anchor
public func save(_ anchor: HBAnchor) async throwslet anchor = HBAnchor(
spaceId: space.id,
transform: entity.transform.matrix,
metadata: ["text": .string("Hello")]
)
try await storage.save(anchor)Load anchors for a space
public func loadAnchors(spaceId: UUID, includeDeleted: Bool = false) async throws -> [HBAnchor]// Active anchors only
let anchors = try await storage.loadAnchors(spaceId: space.id)
// Include soft-deleted anchors
let allAnchors = try await storage.loadAnchors(spaceId: space.id, includeDeleted: true)Load a single anchor
public func loadAnchor(id: UUID) async throws -> HBAnchor?Delete an anchor
public func deleteAnchor(id: UUID) async throwsThis performs a soft delete - the anchor's deletedAt is set but it remains in storage.
Purge deleted anchors
public func purgeDeletedAnchors(before: Date) async throws// Permanently remove anchors deleted more than 30 days ago
let cutoff = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: cutoff)Sync Operations
Manual sync
public func sync() async throwsOnly needed if using .manual sync strategy:
do {
try await storage.sync()
print("Sync complete")
} catch HBStorageError.cloudNotConfigured {
print("Cloud not configured")
}Sync status
// Check if cloud is configured
public var isCloudEnabled: Bool
// Number of pending operations
public var pendingOperationCount: IntVersioning Operations
Get timeline for a space
public func timeline(spaceId: UUID) async throws -> HBTimelinelet timeline = try await storage.timeline(spaceId: space.id)
print("Events: \(timeline.events.count)")
print("Time span: \(timeline.startDate) to \(timeline.endDate)")Get anchors at a point in time
public func anchorsAt(spaceId: UUID, date: Date) async throws -> [HBAnchor]let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
let pastAnchors = try await storage.anchorsAt(spaceId: space.id, date: yesterday)Generate a diff
public func diff(spaceId: UUID, from: Date, to: Date) async throws -> HBDifflet oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
let diff = try await storage.diff(spaceId: space.id, from: oneWeekAgo, to: Date())
print(diff.summary) // "3 added, 1 removed"Get anchor history
public func history(anchorId: UUID) async throws -> [HBAnchorEvent]let events = try await storage.history(anchorId: anchor.id)
for event in events {
print("v\(event.version): \(event.type) at \(event.timestamp)")
}Rollback to previous version
@discardableResult
public func rollback(anchorId: UUID, toVersion: Int) async throws -> HBAnchor// Restore to version 2
let restored = try await storage.rollback(anchorId: anchor.id, toVersion: 2)Rollback creates a new restored event - it doesn't delete history. The full event log is preserved.
Data Management
Clear local storage
public func clearLocalStorage() throwsThis permanently deletes all local data including spaces and anchors. Use with caution.
Get storage size
public func localStorageSize() throws -> IntReturns total size of local storage in bytes.
Storage Size Expectations
Understanding typical storage requirements helps with planning and cleanup:
Size by Content
| Content | Typical Size | Notes |
|---|---|---|
| 1 space (world map) | 3-30 MB | Depends on environment size |
| 1 anchor | 0.5-2 KB | Varies by metadata complexity |
| 100 anchors | 50-200 KB | Negligible compared to spaces |
| 1000 anchors | 0.5-2 MB | Still small vs world maps |
| Pending sync operation | ~1 KB | Queued operations |
Key insight: World maps dominate storage. Anchors are negligible.
Realistic Scenarios
| Scenario | Spaces | Anchors | Est. Total Size |
|---|---|---|---|
| Single room, 10 notes | 1 | 10 | 5-15 MB |
| Home with 3 rooms | 3 | 50 | 15-50 MB |
| Office with many markers | 5 | 200 | 30-100 MB |
| Heavy usage over months | 20 | 1000 | 100-400 MB |
Monitoring Storage
func checkStorageHealth() throws {
let size = try storage.localStorageSize()
let sizeInMB = Double(size) / 1_000_000
print("Storage: \(String(format: "%.1f", sizeInMB)) MB")
if sizeInMB > 100 {
print("Consider cleaning up old spaces")
}
}Cleanup Strategies
Purge Old Deleted Anchors
Soft-deleted anchors remain in storage until explicitly purged:
// Remove anchors deleted more than 30 days ago
let cutoff = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: cutoff)Remove Unused Spaces
Delete spaces that haven't been used recently:
let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: Date())!
for space in try await storage.loadAllSpaces() {
if space.updatedAt < ninetyDaysAgo {
// Check if it has any active anchors
let anchors = try await storage.loadAnchors(spaceId: space.id)
if anchors.isEmpty {
try await storage.deleteSpace(id: space.id)
print("Deleted unused space: \(space.name ?? space.id.uuidString)")
}
}
}Automatic Cleanup on Launch
func performMaintenanceIfNeeded() async throws {
let lastCleanup = UserDefaults.standard.object(forKey: "lastCleanup") as? Date
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
guard lastCleanup == nil || lastCleanup! < oneWeekAgo else { return }
// Purge old deleted anchors
let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: thirtyDaysAgo)
UserDefaults.standard.set(Date(), forKey: "lastCleanup")
}User-Initiated Cleanup
Let users manage their storage:
struct StorageManagementView: View {
@State private var storageSize: Int = 0
@State private var spaces: [HBSpace] = []
var body: some View {
List {
Section("Storage Used") {
Text(ByteCountFormatter.string(
fromByteCount: Int64(storageSize),
countStyle: .file
))
}
Section("Saved Spaces") {
ForEach(spaces) { space in
HStack {
Text(space.name ?? "Unnamed")
Spacer()
Text(space.worldMapSizeFormatted)
.foregroundColor(.secondary)
}
}
.onDelete(perform: deleteSpaces)
}
}
}
}Internal Flow
When save(_ space:) is called:
- •Compress
worldMapDatausing configured compression level - •Create internal
HBStorageSpacewith compression flag - •Save to
HBLocalStore(always) - •If
syncStrategy == .onSaveand cloud is configured:- •Upload to
HBCloudStore - •On failure: queue operation for retry
- •Upload to
When loadSpace(id:) is called:
- •Try loading from
HBLocalStore - •If not found and cloud is configured, try
HBCloudStore - •If found in cloud, cache locally
- •Decompress
worldMapData - •Return
HBSpace
Next Steps
- •Configuration - Storage configuration options
- •Local Storage - How local storage works
- •Cloud Sync - Setting up Supabase
- •Versioning - Timeline navigation and rollback