fix(patterns,performance,browser,ui) - replace global-version and sin… - #59
Merged
Conversation
…gle-handle concurrency with per-operation ownership
…e hidden, order restores by key, and install ownership before callbacks
…make broad operations linear
… a per-row claim chain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Four public helpers tracked concurrent async work with state that cannot represent it: a global version counter, a single mutable native handle, and a plain boolean. Each produced a reproducible failure.
optimisticList()— a global counter cannot answer a per-row question. Rollback was guarded by "has a newer operation started since?", which is wrong in both directions. Skipping the rollback left a failed operation's item on screen permanently:Running it was no better: restoring the captured array discarded every change newer operations had made to rows the failing one never touched. Row identity was also broken — a fallback to
Object.islocated the first equal value, so[1]plus an optimisticadd(1)confirmed as10produced[10, 1].The list is now a ledger of rows with stable ids and per-row operation ownership. Existence and value are owned separately: a failed
addwithdraws its own row whatever its value-ownership has become, while a successfuladdpublishes only if it still owns the value. Disjoint operations never interact; where two touch the same row the later one wins.items()projects values only — no id or wrapper is observable.Chunk invalidation did not stop in-flight work.
invalidate(id)andclear()left the pending map untouched, soload("a") → clear() → old load resolveswrote the discarded value straight back into the cache. The same continuation deleted the pending entry unconditionally, removing a newer load's; and because the stale promise stayed in the map, a post-invalidationload(id, freshLoader)deduplicated against it and never called its own loader.The pending entry object is the load's claim on the key, so removing it from the map revokes ownership — no generation counters, no map that grows. Superseded work still settles for its original caller. Nothing is cancelled and nothing claims to be: the loader API takes no abort signal.
Lifecycle callbacks were participants, not observers. A throwing
onLoadEndturned a cached success into a rejection and delivered its own exception toonLoadError— the caller was told "failed" whileregistry.get(id)returned the value. A throwingonLoadStartstopped the load from starting at all. Callbacks now run contained and report through the runtime error pipeline;onLoadErrorreceives only genuine loader/timeout errors, exactly once.wakeLock()could orphan a native handle. Two overlapping requests acquired two sentinels and kept only the last reference;release()then released the survivor while the other stayed held with nothing left to release it.release()also did nothing about a request already in flight, so a lock could reactivate after being given up, and a stale sentinel'sreleaseevent cleared the current one's state.Requests now share one in-flight acquisition,
release()/dispose()revoke ownership before awaiting anything, and any sentinel arriving without ownership is released immediately — which makes an orphan structurally impossible rather than merely unlikely.viewTransition().isTransitioning()described the last run to finish, not the controller. Two overlapping starts raced over one boolean, so the flag went false while an earlier transition was still running. It now stays true while any run is in flight and becomes false exactly when the last settles, in any order; every caller keeps its own resolution or rejection.Related Issue
Closes #
Type of Change
Breaking: a failed
optimisticListoperation now reverses its own mutation instead of being suppressed by a newer one, so a failed item disappears where it previously persisted. Code that relied on the old suppression will see different arrays.wakeLockrequest/release failures are reported through the runtime error pipeline rather thanconsole.warn.Checklist