Add incremental FilteredList dispatch and batch sync paths#8
Conversation
- FilteredList: dispatch source changes incrementally (Add/Remove single + batch, Replace/Move single) for both filter-only and filter+comparer modes; falls back to full Update() for Reset and batch Replace/Move. - ObservableList: new RemoveRange(start, count) with OnRemovedRange hook; synchronizers (List/Dict/HashSet) propagate batch Add/Remove instead of throwing NotImplementedException; Queue/Stack/Dict use NotSupportedException for actions the underlying collection cannot emit. - CollectionChangedEvent: copy-on-write subscriber list during Invoke so handlers may subscribe/unsubscribe without disturbing in-flight iteration; lazy HashSet tracks unsubscribes during invoke for O(1) skip. - Fix bugs surfaced by self-review: infinite recursion in ObservableStackSync.OnPoppedRange / ObservableQueueSync.OnDequeuedRange overrides (renamed private helpers), wrong value in ObservableDictionarySync.OnReplaced, FilteredList move tie-break drift under comparer, non-injective converter handling in HashSetSync, RemoveRange negative-count validation, FilteredList 3-arg ctor subscribe-before-init window. - Tests: AutoUpdate suite for FilteredList, RemoveRange tests for ObservableList, copy-on-write event tests; performance benchmark scaffold under Tests/Observable/Performance.
- Document Added / Changed / Fixed for the [Unreleased] section: FilteredList incremental dispatch, ObservableList.RemoveRange, batch synchronizer paths, copy-on-write CollectionChangedEvent, and the 9 self-review fixes.
- CreateSyncList: covers InsertRange propagation at the correct index - FilteredList auto-update: InsertRange with and without a comparer - SplitByEvents: added/removed callbacks fire on InsertRange and RemoveRange
- Bump documented package version to 1.0.2 and reconcile Unity target with package.json (2021.3 manifest minimum vs 2022.3+ dev target). - Expand directory tree with Extensions/ subfolders, ObservableCollectionEvents, and Tests/Observable/Performance/. - Add Aspid.Collections.Observable.PerformanceTests.asmdef to the assemblies table and a Gotcha covering its ASPID_COLLECTIONS_PERFORMANCE_TESTING define gate. - Point readers at CHANGELOG.md for in-flight work in the Unreleased block.
- Add Editor/Resources/Icons/aspid_icon_medium_green_1022x1011.png with folder metas - Repoint MonoImporter.icon GUID in every runtime and test .cs.meta to the new icon - Refresh PerformanceTests asmdef formatting (Unity auto-save)
|
@claude once |
|
Claude finished @VPDPersonal's task in 5m 8s —— View job Code Review
|
When the remove accessor for CollectionChanged forked _handlers during an active invoke but the handler was not present, the fork was still assigned back to _handlers. This silently discarded any concurrent additions made to the original list (e.g. via a fresh fork from add) and left an unchanged copy in its place. Move the assignment inside the Remove check so we only swap when the handler was actually removed.
The CleanUp lambda in Remove_Middle_ComparerIsSlowerThanNoComparer only disposed the FilteredList wrapper; the underlying ObservableList stayed alive across measurement iterations. Dispose the source too so each iteration starts from a clean state. https://claude.ai/code/session_0133w4Rt2GPd43rjC6UG5Dfg
Throw ArgumentOutOfRangeException before taking the lock, matching the existing count guard, so callers get a deterministic exception instead of an IndexOutOfRangeException from the per-item read at items[i] = _list[startIndex + i].
Validate negative startIndex in ObservableList.RemoveRange

Summary
FilteredList<T>now dispatches source changes incrementally instead of full-rebuilding on every event. Handles Add/Remove (single + batch), Replace and Move (single), with and without filter/comparer. Reset and batch Replace/Move still fall back to a fullUpdate().ObservableList<T>gainsRemoveRange(startIndex, count)plus anOnRemovedRangehook. Synchronizers (Dict / HashSet / List) propagate batch Add/Remove instead of throwingNotImplementedException; Queue / Stack / Dict useNotSupportedExceptionfor actions the underlying collection cannot emit.CollectionChangedEvent<T>is now re-entrancy-safe: the subscriber list is copy-on-written when add/remove happens during an in-flightInvoke, and a lazyHashSet<Handler>tracks unsubscribes-during-invoke for O(1) skip of removed handlers.ObservableStackSync.OnPoppedRange/ObservableQueueSync.OnDequeuedRangeoverrides, wrong value inObservableDictionarySync.OnReplaced(passednewItem.ValuetoOnRemoved),FilteredList.ApplyMoveSingletie-break drift under comparer, non-injective converter guard inObservableHashSetSync,RemoveRangenegative-count validation,FilteredList3-arg ctor subscribe-before-init window.AutoUpdate_*suite for FilteredList (including a randomised parity-vs-full-rebuild test across all four filter × comparer combinations × 200 ops), batchRemoveRangetests, copy-on-write event tests,InsertRange/RemoveRangecoverage forCreateSyncList,FilteredListauto-update andSplitByEvents. Performance benchmark scaffold lives underTests/Observable/Performance/, gated byASPID_COLLECTIONS_PERFORMANCE_TESTING.Editor/Resources/Icons/aspid_icon_medium_green_1022x1011.png; runtime and test.cs.metafiles repointed to it.CLAUDE.md/CLAUDE.md.metafrom the published UPM tree.CHANGELOG.mdupdated under[Unreleased]to cover all of the above;CLAUDE.mdsynced with the current asmdefs, directory layout and package version.Notes for review
.metafiles with only aniconGUID change — Unity script-icon reassignment to the new Aspid icon, no logical impact.ApplyMoveSinglenow always reseats the moved entry — previously it short-circuited for the comparer case, which silently desynced source-index tie-break (visible only for reference types whose comparer returns 0 for distinct instances). The randomised parity test passes because it asserts onintvalues; consumers with custom comparers + reference types are the ones who would have hit the divergence.Test plan
Aspid.Collections.Observable.Testsin Unity Test Runner (EditMode) — full suite.AutoUpdate_ParityWithFullRebuild_RandomSequence.ASPID_COLLECTIONS_PERFORMANCE_TESTINGand runFilteredListPerformanceTests.