Repository navigation
feat(data-structures/unstable): add IntrusiveList - #7346
Closed
tomas-zijdemans wants to merge 1 commit into
Closed
tomas-zijdemans wants to merge 1 commit into
tomas-zijdemans wants to merge 1 commit into
Conversation
Adds `IntrusiveList` and `IntrusiveListNode` at `@std/data-structures/unstable-intrusive-list`. The list stores its links on the caller's objects, so whoever holds a node can remove it or move it to the back in O(1). Channel waiters, the waiters in `@std/rate-limit` (denoland#7237), and Cache recency order (denoland#7236) all need that. Channel and the limiter scan a `Deque` to cancel a waiter today, and Cache hand-writes its links. Nodes extend `IntrusiveListNode`, which keeps the links and the owning list in private fields. Every method that takes a node throws a `TypeError` for a node from another list, and removing a detached node returns `false`, which is what an abort handler wants. The nine members are named like `Deque`. There's no `clear()`, because every consumer visits each node when it empties its list, and an O(1) reset would leave nodes claiming a list they left. Go's `container/list` has that bug (golang/go#39014). I ported all three consumers on local branches. Every existing test passes unchanged. Cancelling 10,000 Channel senders in reverse drops from 73 ms to 5.2 ms, and ordinary delivery is 0 to 7% slower. The limiter looks the same. Cache writes get 10 to 20% slower, mostly because the entry becomes a subclass: Cache's own links on a subclass entry already cost 5 to 15%. Cache's case is one implementation instead of three, not speed. Cache and the limiter adopt in follow-ups once their PRs merge. Channel is stable, so it waits until this is. Look hardest at the iterator. Removing the node it returned last keeps the loop going, and it never walks into another list.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7346 +/- ##
==========================================
+ Coverage 95.04% 95.05% +0.01%
==========================================
Files 619 619
Lines 52012 51898 -114
Branches 9450 9425 -25
==========================================
- Hits 49434 49333 -101
+ Misses 2031 2022 -9
+ Partials 547 543 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Adds
IntrusiveListandIntrusiveListNodeat@std/data-structures/unstable-intrusive-list. The list stores its links onthe caller's objects, so whoever holds a node can remove it or move it to the
back in O(1).
Immediate std consumers::
@std/rate-limit(feat(rate-limit/unstable): add rate limiting module #7237) (waiters)@std/cache(feat(cache/unstable): addCache#7236) (recency order)Later, maybe:
Semaphorein@std/async/unstable-semaphorekeeps its own waiter queue. Ifacquire()gets asignaloption, a cancelled waiter has to leave the middle of that queue, which is what this list does in O(1).An intrusive list keeps its
prevandnextlinks on the elements themselves,not in wrapper nodes the list allocates. Code that holds an element can unlink
it in O(1) without a search, and the list allocates nothing per element. It's the
usual structure for wait queues and LRU orders:
list.h,intrusive-collections,LinkedList<T>JavaScript has no built-in one (Node has internal/linkedlist.js which Deno ports). Libraries that need it write their own. Cache did.
Nodes extend
IntrusiveListNode, which keeps the links and the owning list inprivate fields. Every method that takes a node throws a
TypeErrorfor anode from another list, and removing a detached node returns
false. There's noclear(),because every consumer visits each node when it empties its list, and an O(1)
reset would leave nodes claiming a list they left.
I used Claude Code to help investigate and write this change.