Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,16 @@ class MyDelegate: IndexStoreDelegate {
print("Out-of-date unit detected: \(unit.unitName) — \(unit.triggerHintDescription)")
}

func indexStore(_ store: IndexStore, didUpdateTrackedUnitStatus trackedUnit: TrackedUnit) {
print("Unit '\(trackedUnit.unit.unitName)' status changed to \(trackedUnit.status)")
func indexStore(_ store: IndexStore, didProcessOutOfDateUnit trackedUnit: TrackedUnit) {
print("Unit '\(trackedUnit.unit.unitName)' processed — status: \(trackedUnit.status)")
}
}

let delegate = MyDelegate()
indexStore.delegate = delegate
```

All delegate methods are optional except `didUpdatePendingUnitCount` and `didDetectOutOfDateUnit`. The `didUpdateTrackedUnitStatus` callback has a default empty implementation so existing adopters are not broken.
All delegate methods are optional except `didUpdatePendingUnitCount` and `didDetectOutOfDateUnit`. The `didProcessOutOfDateUnit` callback has a default empty implementation so existing adopters are not broken.

**Inspecting out-of-date units:**

Expand Down Expand Up @@ -229,7 +229,7 @@ let selected = staleUnits.filter { $0.unit.unitName.contains("MyModule") }
indexStore.processOutOfDateUnits(selected)
```

Each unit transitions through `.processing` (while being re-imported) and then `.processed` once complete. The delegate is notified at each transition.
Each unit transitions through `.processing` (while being re-imported) and then `.processed` once complete. The delegate's `didProcessOutOfDateUnit` callback fires when the unit reaches `.processed`.

**Clearing processed history:**

Expand Down
10 changes: 8 additions & 2 deletions Sources/IndexStore/IndexStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public protocol IndexStoreDelegate: AnyObject {
/// - pendingUnitCount: The current computed pending count (never negative).
func indexStore(_ store: IndexStore, didUpdatePendingUnitCount pendingUnitCount: Int)

/// Called when indexstore-db detects an out-of-date unit (only if out-of-date watching is enabled).
/// Called when indexstore-db detects an out-of-date unit.
///
/// This callback requires ``IndexStore/Configuration/enableOutOfDateFileWatching`` to be `true` (the default).
///
/// - Parameters:
/// - store: The ``IndexStore`` instance that detected the out-of-date unit.
Expand All @@ -32,6 +34,7 @@ public protocol IndexStoreDelegate: AnyObject {
///
/// This fires once per unit at the end of the ``IndexStore/processOutOfDateUnits(_:)`` lifecycle,
/// after the unit's status has transitioned to ``UnitInfo/Status/processed``.
/// Requires ``IndexStore/Configuration/enableOutOfDateFileWatching`` to be `true` (the default).
///
/// - Parameters:
/// - store: The ``IndexStore`` instance that processed the unit.
Expand Down Expand Up @@ -244,6 +247,7 @@ public final class IndexStore {

/// Returns `true` when there is at least one tracked unit with ``UnitInfo/Status/outOfDate`` status.
///
/// Requires ``Configuration/enableOutOfDateFileWatching`` to be `true` (the default) for units to be tracked.
/// This is a lightweight check that avoids copying the full array — prefer this over
/// `outOfDateUnits.isEmpty` when you only need a boolean signal.
public var hasOutOfDateUnits: Bool {
Expand All @@ -269,12 +273,14 @@ public final class IndexStore {

/// Processes specific out-of-date units by re-importing their data into the index store.
///
/// Requires ``Configuration/enableOutOfDateFileWatching`` to be `true` (the default) for units to be tracked.
///
/// The lifecycle for each unit is:
/// 1. Status transitions to ``UnitInfo/Status/processing``
/// 2. The unit's `mainFilePath` is passed to the underlying `processUnitsForOutputPathsAndWait`
/// 3. Status transitions to ``UnitInfo/Status/processed``
///
/// The ``IndexStoreDelegate/indexStore(_:didUpdateTrackedUnitStatus:)`` callback fires at each transition.
/// The ``IndexStoreDelegate/indexStore(_:didProcessOutOfDateUnit:)`` callback fires when a unit reaches ``UnitInfo/Status/processed``.
///
/// - Parameter units: The ``TrackedUnit`` values to process. Typically retrieved from ``outOfDateUnits``.
public func processOutOfDateUnits(_ units: [TrackedUnit]) {
Expand Down
15 changes: 6 additions & 9 deletions Sources/IndexStore/Internal/Status/IndexStatusController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ class IndexStatusController: IndexDelegate {

// MARK: - Helpers: Tracked Unit Lifecycle

/// Transitions the given unit names from ``UnitInfo/Status/outOfDate`` to ``UnitInfo/Status/processing``.
/// Transitions the given unit names back to ``UnitInfo/Status/outOfDate``.
///
/// Only units whose current status is `.outOfDate` will be transitioned. Units in any other
/// status are silently ignored.
/// Only units that are already tracked and not currently `.outOfDate` will be transitioned.
/// Units that are already `.outOfDate` or not tracked are silently ignored.
///
/// - Parameter unitNames: The set of `unitName` identifiers to transition.
/// - Returns: The ``TrackedUnit`` values that were actually transitioned. Empty if none matched.
Expand All @@ -203,7 +203,7 @@ class IndexStatusController: IndexDelegate {
countQueue.sync {
for name in unitNames {
guard let existing = state.trackedUnits[name], existing.status != .outOfDate else { continue }
let updated = existing.withStatus(.processing)
let updated = existing.withStatus(.outOfDate)
state.trackedUnits[name] = updated
transitioned.append(updated)
}
Expand All @@ -229,9 +229,6 @@ class IndexStatusController: IndexDelegate {
transitioned.append(updated)
}
}
for unit in transitioned {
notifyProcessedOutOfDateUnit(unit)
}
return transitioned
}

Expand Down Expand Up @@ -275,8 +272,6 @@ class IndexStatusController: IndexDelegate {
}
}

// MARK: - Notifications (internal)

/// Clears the ``lastOutOfDateUnit`` and ``lastOutOfDateTimestamp`` values.
///
/// This does **not** affect the ``trackedUnits`` collection.
Expand All @@ -287,6 +282,8 @@ class IndexStatusController: IndexDelegate {
}
}

// MARK: - Notifications (internal)

/// Forwards the current pending unit count to the public ``IndexStoreDelegate``.
func notifyPendingCountChanged() {
guard let store else { return }
Expand Down
21 changes: 9 additions & 12 deletions Tests/IndexStoreTests/IndexStore/IndexStatusControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,26 +292,23 @@ final class IndexStatusControllerTests: XCTestCase {
}

func test_statusTransitions_notifyDelegate() {
// Reset the spy count (unitIsOutOfDate also fires a notification)
delegateSpy.indexStore_didProcessOutOfDateUnitCallCount = 0
delegateSpy.indexStore_didProcessOutOfDateUnitParameterList.removeAll()

reportOutOfDateUnit(mainFilePath: "pathA", unitName: "unitA")
// 1 notification: outOfDate
instanceUnderTest.markUnitsAsOutOfDate(["unitA"])

// unitIsOutOfDate should NOT fire didProcessOutOfDateUnit
XCTAssertFalse(delegateSpy.indexStore_didProcessOutOfDateUnitCalled)

// Transitioning to .processing should NOT fire didProcessOutOfDateUnit
instanceUnderTest.markUnitsAsProcessing(["unitA"])
// 2 notifications total: outOfDate + processing
XCTAssertEqual(delegateSpy.indexStore_didProcessOutOfDateUnitCallCount, 1)
XCTAssertEqual(delegateSpy.indexStore_didProcessOutOfDateUnitParameters?.trackedUnit.status, .processing)
XCTAssertFalse(delegateSpy.indexStore_didProcessOutOfDateUnitCalled)

// Transitioning to .processed SHOULD fire didProcessOutOfDateUnit
instanceUnderTest.markUnitsAsProcessed(["unitA"])
// 3 notifications total: outOfDate + processing + processed
XCTAssertEqual(delegateSpy.indexStore_didProcessOutOfDateUnitCallCount, 2)
XCTAssertEqual(delegateSpy.indexStore_didProcessOutOfDateUnitCallCount, 1)
XCTAssertEqual(delegateSpy.indexStore_didProcessOutOfDateUnitParameters?.trackedUnit.status, .processed)
XCTAssertIdentical(delegateSpy.indexStore_didProcessOutOfDateUnitParameters?.store, indexStore)

// All delegate calls should reference the correct store
// The didDetectOutOfDateUnit delegate should have fired once (from unitIsOutOfDate)
XCTAssertEqual(delegateSpy.indexStore_didDetectOutOfDateUnitCallCount, 1)
XCTAssertTrue(delegateSpy.indexStore_didDetectOutOfDateUnitParameterList.allSatisfy { $0.store === indexStore })
}

Expand Down