Skip to content

feat(data-structures/unstable): add IntrusiveList - #7346

Closed
tomas-zijdemans wants to merge 1 commit into
denoland:mainfrom
tomas-zijdemans:feat-intrusive-list
Closed

tomas-zijdemans wants to merge 1 commit into
denoland:mainfrom
tomas-zijdemans:feat-intrusive-list

Conversation

@tomas-zijdemans

@tomas-zijdemans tomas-zijdemans commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

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).

Immediate std consumers::

Later, maybe: Semaphore in @std/async/unstable-semaphore keeps its own waiter queue. If acquire() gets a signal option, 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 prev and next links 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:

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 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. 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.

I used Claude Code to help investigate and write this change.

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

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.05%. Comparing base (f834d02) to head (09b2790).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tomas-zijdemans
tomas-zijdemans deleted the feat-intrusive-list branch October 11, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant