HyperbasisHyperbasisDocs
Home

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 TypeWhen it occurs
createdAnchor first saved
movedTransform changed
updatedMetadata changed
deletedAnchor deleted
restoredAnchor 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

PropertyTypeDescription
idUUIDUnique event identifier
anchorIdUUIDThe anchor this event affects
spaceIdUUIDParent space ID
typeEventTypecreated, moved, updated, deleted, restored
timestampDateWhen the event occurred
versionIntSequential version number for this anchor
transform[Float]?Transform snapshot (for created/moved/restored)
metadata[String: AnyCodableValue]?Metadata snapshot (for created/updated/restored)
actorIdString?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

PropertyTypeDescription
spaceIdUUIDThe space this timeline represents
events[HBAnchorEvent]All events in chronological order
startDateDate?Earliest event timestamp
endDateDate?Latest event timestamp
durationTimeInterval?Total time span
anchorIdsSet<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

PropertyTypeDescription
spaceIdUUIDSpace this diff applies to
fromDateDateStart of diff range
toDateDateEnd 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` date

MovedAnchor

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 created event
  • Event uses the anchor's original createdAt date
  • 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)

Next Steps

  • HBStorage - Storage API including versioning methods
  • Examples - See versioning in action