Versioning
Track anchor history and navigate through time.
Overview
Hyperbasis uses event sourcing to record every change to anchors. This enables:
- •Viewing anchor states at any point in time
- •Generating diffs between two dates
- •Rolling back anchors to previous versions
- •Tracking the full history of changes
Every anchor operation (create, move, update, delete, restore) is recorded as an immutable event.
Key Concepts
Events
Every change is recorded as an HBAnchorEvent:
| Event Type | When it occurs |
|---|---|
created | Anchor first saved |
moved | Transform changed |
updated | Metadata changed |
deleted | Anchor deleted |
restored | Anchor restored from deletion or rollback |
Timeline
HBTimeline provides navigation through a space's history:
- •Reconstruct anchor states at any date
- •Generate diffs between two points
- •Query events by type, anchor, or date range
Diff
HBDiff describes changes between two points in time:
- •Added anchors
- •Removed anchors
- •Moved anchors (with previous transform)
- •Updated anchors (with previous metadata)
- •Unchanged anchors
Usage
Get Timeline for a Space
let timeline = try await storage.timeline(spaceId: space.id)
// Timeline properties
print("Start: \(timeline.startDate)")
print("End: \(timeline.endDate)")
print("Total events: \(timeline.events.count)")
print("Unique anchors: \(timeline.anchorIds.count)")View State at a Point in Time
// Get anchors as they existed yesterday
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
let anchors = try await storage.anchorsAt(spaceId: space.id, date: yesterday)
for anchor in anchors {
let position = anchor.position
print("Anchor at \(position)")
}Generate a Diff
let 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, 2 moved"
// Inspect specific changes
for added in diff.added {
print("New anchor: \(added.id)")
}
for moved in diff.moved {
print("Moved \(moved.distanceMoved)m: \(moved.anchor.id)")
}
for updated in diff.updated {
print("Changed keys: \(updated.changedKeys)")
}Get Anchor History
let events = try await storage.history(anchorId: anchor.id)
for event in events {
print("\(event.timestamp): \(event.type) (v\(event.version))")
}Rollback to Previous Version
// Restore anchor to version 3
let restored = try await storage.rollback(anchorId: anchor.id, toVersion: 3)
print("Restored to v3: \(restored.metadata)")HBAnchorEvent
Properties
| Property | Type | Description |
|---|---|---|
id | UUID | Unique event identifier |
anchorId | UUID | The anchor this event affects |
spaceId | UUID | Parent space ID |
type | EventType | created, moved, updated, deleted, restored |
timestamp | Date | When the event occurred |
version | Int | Sequential version number for this anchor |
transform | [Float]? | Transform snapshot (for created/moved/restored) |
metadata | [String: AnyCodableValue]? | Metadata snapshot (for created/updated/restored) |
actorId | String? | Optional user/device identifier |
Event Types
public enum EventType: String, Codable {
case created // Anchor was created
case moved // Transform changed
case updated // Metadata changed
case deleted // Anchor was deleted
case restored // Anchor restored from deletion or rollback
}HBTimeline
Initialization
// Created via HBStorage
let timeline = try await storage.timeline(spaceId: space.id)Properties
| Property | Type | Description |
|---|---|---|
spaceId | UUID | The space this timeline represents |
events | [HBAnchorEvent] | All events in chronological order |
startDate | Date? | Earliest event timestamp |
endDate | Date? | Latest event timestamp |
duration | TimeInterval? | Total time span |
anchorIds | Set<UUID> | All unique anchor IDs |
Methods
// Query events
func events(for anchorId: UUID) -> [HBAnchorEvent]
func events(from: Date, to: Date) -> [HBAnchorEvent]
func events(ofType type: EventType) -> [HBAnchorEvent]
// State reconstruction
func state(at date: Date) -> [HBAnchor]
// Diff generation
func diff(from: Date, to: Date) -> HBDiff
// UI helpers
func scrubberDates(from: Date?, to: Date?, steps: Int) -> [Date]
func closestEvent(to date: Date) -> HBAnchorEvent?
var significantDates: [Date]HBDiff
Properties
| Property | Type | Description |
|---|---|---|
spaceId | UUID | Space this diff applies to |
fromDate | Date | Start of diff range |
toDate | Date | End of diff range |
added | [HBAnchor] | Anchors in to but not from |
removed | [HBAnchor] | Anchors in from but not to |
moved | [MovedAnchor] | Anchors whose transform changed |
updated | [UpdatedAnchor] | Anchors whose metadata changed |
unchanged | [HBAnchor] | Anchors that didn't change |
Computed Properties
var changeCount: Int // Total number of changes
var hasChanges: Bool // Whether any changes occurred
var summary: String // "3 added, 1 removed, 2 moved"
var currentAnchors: [HBAnchor] // All anchors at `to` date
var previousAnchors: [HBAnchor] // All anchors at `from` dateMovedAnchor
struct MovedAnchor {
let anchor: HBAnchor // Current state
let previousTransform: [Float] // Transform at `from` date
var previousPosition: SIMD3<Float> // Extracted from transform
var currentPosition: SIMD3<Float> // From current anchor
var distanceMoved: Float // Distance in meters
}UpdatedAnchor
struct UpdatedAnchor {
let anchor: HBAnchor
let previousMetadata: [String: AnyCodableValue]
var addedKeys: Set<String> // New keys
var removedKeys: Set<String> // Deleted keys
var changedKeys: Set<String> // Keys with different values
}Storage Size
Events are stored as JSONL (one JSON object per line):
- •Each event: ~200-500 bytes
- •Much smaller than world map snapshots (5-50MB)
- •Append-only for performance
Migration
Existing anchors from Phase 1 are automatically migrated:
- •First save after upgrade creates a
createdevent - •Event uses the anchor's original
createdAtdate - •No action required from developers
Use Cases
Time Machine UI
struct TimeMachineView: View {
@State private var scrubberPosition: Double = 1.0
@State private var currentAnchors: [HBAnchor] = []
let timeline: HBTimeline
let scrubberDates: [Date]
var body: some View {
VStack {
// AR View showing currentAnchors
ARViewContainer(anchors: currentAnchors)
// Time scrubber
Slider(value: $scrubberPosition, in: 0...1)
.onChange(of: scrubberPosition) { _, newValue in
let index = Int(newValue * Double(scrubberDates.count - 1))
let date = scrubberDates[index]
currentAnchors = timeline.state(at: date)
}
}
}
}Change Summary
func showRecentChanges() async {
let lastWeek = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
let diff = try await storage.diff(spaceId: space.id, from: lastWeek, to: Date())
if diff.hasChanges {
print("Last 7 days: \(diff.summary)")
}
}Undo Last Change
func undoLastChange(for anchorId: UUID) async throws {
let events = try await storage.history(anchorId: anchorId)
guard events.count > 1 else { return } // Need at least 2 versions
let previousVersion = events[events.count - 2].version
try await storage.rollback(anchorId: anchorId, toVersion: previousVersion)
}Errors
New versioning-related errors:
case versionNotFound(anchorId: UUID, version: Int)
case reconstructionFailed(anchorId: UUID)
case eventLogCorrupted(spaceId: UUID)