Skip to content

fix: eliminate Promise.race handler-stacking in batchPackageStream#600

Merged
John-David Dalton (jdalton) merged 4 commits intomainfrom
fix/promise-race-leak
Apr 21, 2026
Merged

fix: eliminate Promise.race handler-stacking in batchPackageStream#600
John-David Dalton (jdalton) merged 4 commits intomainfrom
fix/promise-race-leak

Conversation

@jdalton
Copy link
Copy Markdown
Contributor

Summary

  • Fix (src/socket-sdk-class.ts): batchPackageStream re-raced a persistent pool of generator promises each iteration. Every Promise.race(running.values()) call attached a fresh .then(resolve, reject) pair to every still-pending arm, so long-running generators accumulated O(iterations_survived) dead handler closures before finally settling. See nodejs/node#17469 and @watchable/unpromise for the pattern.
  • Replaced with a single-waiter queue: each generator's own .then delivers its step into a buffer, and the main loop awaits a fresh promiseWithResolvers each iteration. Handlers are one-shot — nothing to stack. Rejection and ordering semantics preserved.
  • Docs (CLAUDE.md): added a concise rule under SHARED STANDARDS so the pattern is not reintroduced.

Test plan

  • All 565 existing tests pass (pnpm test)
  • pnpm run check (lint + type-check) clean
  • Verified via standalone sanity script: result ordering preserved, rejection propagation preserved

The prior implementation stored per-generator promises in a Map and
called `Promise.race(running.values())` every loop iteration. Each
race call re-attached fresh `.then` handlers to every still-pending
promise in the pool, so long-surviving generators accumulated
O(iterations) dead handler closures before finally settling.

See nodejs/node#17469 and
https://github.com/cefn/watchable/tree/main/packages/unpromise for
the pattern.

Switch to a single-waiter queue: each generator's `.then` delivers
its step into a buffer, and the main loop awaits a fresh
promiseWithResolvers each iteration. Handlers are one-shot —
nothing to stack.

All 565 tests pass.
Pairs with the batchPackageStream fix: documents the anti-pattern
so future code does not reintroduce it. Concise bullet in the
SHARED STANDARDS section alongside the existsSync rule.
The rule already lives in SHARED STANDARDS (line 26). Having it twice
— once general, once under sdk-specific TypeScript Patterns — adds
maintenance drift without adding clarity. Keep the SHARED STANDARDS
version.
…ckageStream

Align the single-waiter queue with project rules: alphabetical
destructuring/type property order and undefined (not null) for
absent state.
@jdalton John-David Dalton (jdalton) merged commit 376a743 into main Apr 21, 2026
11 checks passed
@jdalton John-David Dalton (jdalton) deleted the fix/promise-race-leak branch April 21, 2026 13:02
John-David Dalton (jdalton) added a commit that referenced this pull request Apr 21, 2026
Replace the 6-line comment landed in #600 with the fuller narrative that
was sitting uncommitted on the fix/promise-race-leak worktree — concrete
walkthrough of the handler-stacking trap, what each piece of the
single-waiter machinery is responsible for, and why the snapshot-and-
clear dance in deliverStep/deliverError matters.

No behavioral change; comments only. The mechanical fix already shipped
in #600 (376a743); this captures the reasoning someone will want when
they look at this code six months from now.
John-David Dalton (jdalton) added a commit that referenced this pull request Apr 21, 2026
* docs(claude): consolidate CLAUDE.md rules from stale branches

Fold the genuinely valuable guidance scattered across several now-stale
worktree branches (fix/promise-race-leak, chore/parallel-claude-rule,
docs/claude-fix-warnings-rule) into a single coherent pass on CLAUDE.md,
and pull in two rules the socket-repo-template gold standard already
carries but this repo was missing.

- Restructure the Promise.race guidance into a dedicated "### Promise.race
  in loops" subsection with the template's Safe/Leaky/Fix triad — same
  content, but the three-state framing is easier to apply at review time
  than a single run-on bullet.
- Add null-prototype rule: `{ __proto__: null, ...rest }` is already the
  idiom used throughout src/socket-sdk-class.ts and src/file-upload.ts
  (config objects, internal state, return shapes); codify it so new
  contributions follow suit and prototype pollution surface stays small.
- Add Linear-reference rule: keep code and PR history tool-agnostic.
  Forward-looking — no violations currently in the repo.

Deliberately NOT pulled from template: the "NEVER use dynamic imports"
rule. SDK uses `await import('node:fs')` legitimately in two places
(socket-sdk-class.ts:1931, :3818) for lazy loading; a blanket ban would
be wrong here.

* docs(sdk): expand batchPackageStream Promise.race rationale

Replace the 6-line comment landed in #600 with the fuller narrative that
was sitting uncommitted on the fix/promise-race-leak worktree — concrete
walkthrough of the handler-stacking trap, what each piece of the
single-waiter machinery is responsible for, and why the snapshot-and-
clear dance in deliverStep/deliverError matters.

No behavioral change; comments only. The mechanical fix already shipped
in #600 (376a743); this captures the reasoning someone will want when
they look at this code six months from now.
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.

2 participants