Skip to content

feat(TextureIGListKitExtensions): add performUpdatesWithFallback with pre-flight section-count guard#14

Merged
3a4oT merged 1 commit into
developmentfrom
feat/safe-perform-updates
May 14, 2026
Merged

feat(TextureIGListKitExtensions): add performUpdatesWithFallback with pre-flight section-count guard#14
3a4oT merged 1 commit into
developmentfrom
feat/safe-perform-updates

Conversation

@3a4oT

@3a4oT 3a4oT commented May 14, 2026

Copy link
Copy Markdown
Owner

Summary

Adds ListAdapter.performUpdatesWithFallback(animated:completion:) (plus an async overload) that performs a pre-flight check UICollectionView.numberOfSections == adapter.objects().count and falls back to reloadData(completion:) on divergence, instead of letting IGListKit raise NSInternalInconsistencyException from IGListBatchUpdateTransaction._didDiff:onBackground: with

The UICollectionView's section count (N) didn't match the IGListAdapter's count (M), so we can't performBatchUpdates. Falling back to reloadData.

(IGListKit's message text says "falling back to reloadData", but IGAssert raises an NSException in release builds — the documented fallback never executes.)

Scope

Catches already-divergent states, where the collection view was reset externally (reloadData from another path, data source reassignment, view re-layout that lost its section count) while the adapter still holds the previous applied snapshot.

Out of scope

Does not prevent the same exception when triggered by a concurrent in-flight diff: IGListKit captures the sectionMap inside its background diff queue, and a second performUpdates that mutates the data source between snapshot and apply still raises from _didDiff:onBackground:. Callers must serialize concurrent loads on the same adapter — this guard is a defence-in-depth layer, not a replacement for caller-side serialization.

Also out of scope: ASCollectionInvalidUpdateException raised from ASDataController.mm:598 when a changeSet validates against a data source already mutated by a subsequent load. That race requires caller-side serialization of the load → performUpdates cycle.

Tests

IGListAdapterBridgeTests adds:

  • performUpdatesWithFallback_appliesDiffs_whenSectionCountsMatch — happy path, ensures the guard does not regress normal consecutive diffs.
  • performUpdatesWithFallback_isNoOp_whenNoCollectionViewAttached — verifies the nil-collectionView fall-through to performUpdates.

The fallback branch is not exercised by a dedicated test because the IGListKit + AsyncDisplayKit API contract intentionally prevents callers from constructing the divergent precondition (dataSource swaps crash on interop selectors; IGListAdapter is not open for subclassing outside its defining module; method-swizzling UICollectionView.numberOfSections would leak across the surrounding regression tests). The branch is one if statement and is verified by inspection; an inline comment in the test file documents this rationale.

Local run on iPhone 17 / iOS 26.4.1 simulator: 9/9 tests passed (7 existing regression + 2 new).

Observability

Divergence events are logged via os_log at .error level under subsystem: TextureIGListKitExtensions, category: iglistkit-guard. Consumers can filter for these in Console.app or pipe them into their structured-logging stack to measure how often the fallback fires in production.

Test plan

  • Verify CI passes on the SPMWithIGListKit test scheme (iOS Simulator).
  • Confirm public API addition is acceptable (new method on ListAdapter) — no breaking changes to existing API.
  • Tag a patch release (e.g. 3.3.5) once merged so SPM consumers can pin the new revision.

… pre-flight section-count guard

Adds ListAdapter.performUpdatesWithFallback(animated:completion:) and an
async overload. Performs a pre-flight check that
UICollectionView.numberOfSections matches IGListAdapter.objects().count; on
divergence, falls back to reloadData(completion:) instead of letting
IGListKit raise NSInternalInconsistencyException from
IGListBatchUpdateTransaction._didDiff:onBackground: with the message

    The UICollectionView's section count (N) didn't match the
    IGListAdapter's count (M), so we can't performBatchUpdates.
    Falling back to reloadData.

(IGListKit's message text says "falling back to reloadData", but IGAssert
raises an NSException in release builds — the documented fallback never
executes.)

Scope:

  Catches already-divergent states where the collection view was reset
  externally (reloadData from another path, data source reassignment, view
  re-layout that lost its section count) while the adapter still holds the
  previous applied snapshot.

Out of scope:

  Does not prevent the same exception when triggered by a concurrent
  in-flight diff. IGListKit captures the sectionMap inside its background
  diff queue; a second performUpdates that mutates the data source between
  snapshot and apply still raises from _didDiff:onBackground:. Callers must
  serialize concurrent loads on the same adapter — this guard is a
  defence-in-depth layer, not a replacement for caller-side serialization.

Tests:

  IGListAdapterBridgeTests adds two regression tests:

    - performUpdatesWithFallback_appliesDiffs_whenSectionCountsMatch —
      happy path, ensures the guard does not regress normal consecutive
      diffs.
    - performUpdatesWithFallback_isNoOp_whenNoCollectionViewAttached —
      verifies the nil-collectionView fall-through to performUpdates.

  The fallback branch is not exercised by a dedicated test because the
  IGListKit + AsyncDisplayKit API contract intentionally prevents callers
  from constructing the divergent precondition: dataSource swaps crash on
  interop selectors, IGListAdapter is not open for subclassing outside its
  defining module, and method-swizzling UICollectionView.numberOfSections
  leaks across the surrounding regression tests. The branch is one if
  statement and is verified by inspection; an inline comment in the test
  file documents this rationale.

Observability:

  Divergence events are logged via os_log at .error level under
  subsystem: TextureIGListKitExtensions, category: iglistkit-guard.
  Consumers can filter for these in Console.app or pipe them into their
  structured-logging stack to measure how often the fallback fires in
  production.
@3a4oT 3a4oT force-pushed the feat/safe-perform-updates branch from 39941eb to 7e80a2d Compare May 14, 2026 12:33
@3a4oT 3a4oT changed the title feat(TextureIGListKitExtensions): add safePerformUpdates with pre-flight section-count guard feat(TextureIGListKitExtensions): add performUpdatesWithFallback with pre-flight section-count guard May 14, 2026
@3a4oT 3a4oT merged commit 4055906 into development May 14, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant