Skip to content

feat: enable HTTP/2 and restore keep-alive in Node transport#3

Merged
amix merged 2 commits into
mainfrom
amix/http2-and-keepalive
May 21, 2026
Merged

feat: enable HTTP/2 and restore keep-alive in Node transport#3
amix merged 2 commits into
mainfrom
amix/http2-and-keepalive

Conversation

@amix
Copy link
Copy Markdown
Member

@amix amix commented May 21, 2026

Context

Follow-up to #2: today the SDK's Node transport doesn't actually use HTTP/2. EnvHttpProxyAgent was constructed without allowH2, and keepAliveTimeout was set to 1ms to keep CLIs exiting fast — so every Node request opened a fresh TCP+TLS connection.

What was changed

  • EnvHttpProxyAgent({ allowH2: true }) — ALPN-negotiated h2 with automatic h1.1 fallback.
  • Dropped the 1ms keep-alive hack; use undici defaults so the pool persists across requests.
  • closeDefaultDispatcher() (package root) and CommsApi.close() so CLIs/scripts can drain the pool before exit.
  • Race fix: clear the singleton before awaiting init in close().
  • README has the lifecycle contract.

Benchmark (staging)

Step Time
users.getSessionUser (first call, TLS handshake) 377ms
workspaces.getWorkspace (reuses connection) 423ms
burst x5 in parallel (5 multiplexed h2 streams, 1 connection) 497ms
threads.createThread 894ms
comments.createComment 816ms
threads.deleteThread 761ms
api.close() 4ms

Out of scope

  • Backend rate-limit accounting (multiplexing doesn't preserve "1 batch = 1 hit" semantics).
  • Doistbot's instance-scoped-dispatcher suggestion — declined. Process-global is intentional so SDK clients and OAuth helpers share one pool; fragmenting it would undo most of the gain.
  • Doistbot's shared singleton-reset helper — pre-existing style duplication, deferred.

@amix amix marked this pull request as ready for review May 21, 2026 10:25
@amix amix requested a review from doistbot May 21, 2026 10:26
@amix amix force-pushed the amix/http2-and-keepalive branch from 7482c2f to 2abffb1 Compare May 21, 2026 10:28
@scottlovegrove
Copy link
Copy Markdown
Collaborator

Might be worth getting @goncalossilva's view on this too, as he added the dispatcher stuff initially into the SDKs.

Backs up the "h2 multiplexing replaces /batch" claim from #2 for Node
consumers (browsers already negotiate h2 automatically). Without these,
Node consumers — including the MCP — were stuck on HTTP/1.1 with
near-disabled keep-alive, so every request opened a fresh TCP+TLS
connection.

Changes:
- `EnvHttpProxyAgent({ allowH2: true })` so ALPN can pick h2 when the
  server offers it (Comms via CloudFront does; fallback to h1.1 is
  automatic when it doesn't).
- Drop the 1ms keepAliveTimeout/keepAliveMaxTimeout hack; use undici
  defaults so the connection pool actually persists across requests.
- Wrap the full dispatcher construction in
  `suppressExperimentalWarningsSync` so the `allowH2` experimental
  warning is swallowed alongside the decompress one.
- Add `closeDefaultDispatcher()` (exported from the package root) and
  `CommsApi.close()` so short-lived CLIs / scripts — including ones
  that only use the OAuth helpers — can drain the pool before exit.

Validated direction with expert-letter (Claude + Codex) and Doistbot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@amix amix force-pushed the amix/http2-and-keepalive branch from 2abffb1 to 9ec9bd1 Compare May 21, 2026 10:28
Copy link
Copy Markdown
Member

@doistbot doistbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR enables HTTP/2 multiplexing and restores proper keep-alive behavior in the Node transport while providing a mechanism to drain the connection pool before exit. These changes significantly improve transport efficiency for Node consumers by eliminating constant TCP/TLS handshakes. There are a few architectural and concurrency details to refine, specifically resolving a race condition during shutdown, clarifying the global versus instance-level scope of the dispatcher, and strengthening the related test coverage.

Share FeedbackReview Logs

Comment thread src/transport/http-dispatcher.ts Outdated
Comment thread src/comms-api.ts
Comment thread src/transport/http-dispatcher.test.ts
@amix
Copy link
Copy Markdown
Member Author

amix commented May 21, 2026

I also wonder if we should patch todoist-sdk-typescript?

I don’t think these HTTP hacks we do are optimal; we should really just adopt HTTP/2 🤔

Clear the singleton before awaiting init. Previously,
`await defaultDispatcherPromise` yielded to the event loop with the
singleton still set; a concurrent `getDefaultDispatcher()` could grab
the old dispatcher reference and then have it torn down underneath it.
Per Doistbot review on #3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@amix amix added the 👀 Show PR PR must be reviewed before or after merging label May 21, 2026
@amix amix merged commit 521c5c2 into main May 21, 2026
6 checks passed
@scottlovegrove
Copy link
Copy Markdown
Collaborator

I also wonder if we should patch todoist-sdk-typescript?

Does the Todoist backend support HTTP/2 then?

@scottlovegrove scottlovegrove deleted the amix/http2-and-keepalive branch May 21, 2026 11:19
@amix
Copy link
Copy Markdown
Member Author

amix commented May 21, 2026

@scottlovegrove yep, it does 👍 This will have pretty big impact on REST endpoints

@goncalossilva
Copy link
Copy Markdown
Member

I don't have a strong opinion here, apart from preferring defaults whenever possible (so no code/config whenever possible). Every change we make adds more code on top. 😅 But it must be, it must be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👀 Show PR PR must be reviewed before or after merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants