Skip to content

types: declare RedisClient.pubsub() and RedisClient.select() - #39208

Open
robobun wants to merge 1 commit into
mainfrom
farm/8837b808/redis-types-pubsub-select
Open

types: declare RedisClient.pubsub() and RedisClient.select()#39208
robobun wants to merge 1 commit into
mainfrom
farm/8837b808/redis-types-pubsub-select

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • RedisClient.prototype.pubsub and RedisClient.prototype.select work at runtime, but packages/bun-types/redis.d.ts does not declare either, so tsc reports Property 'pubsub' does not exist on type 'RedisClient' (same for select) on calls that succeed when run.
  • Both methods have been registered since Bun.redis was introduced (src/runtime/valkey_jsc/valkey.classes.ts:568 and :580, implemented by cmd_strings_varargs! at src/runtime/valkey_jsc/js_valkey_functions.rs:1606 and :1710); the declarations were never added.
  • Of the 167 prototype methods in valkey.classes.ts, these are the only two undeclared ones that are not already covered by an open PR (script is in redis: add bitmap, HLL, geo, scripting, server, and stream commands #29339; psubscribe/punsubscribe are in redis: implement psubscribe/punsubscribe with listener routing #35521, which also adds the missing message delivery they need).

Fix

  • select(index: number | string): Promise<"OK">. The runtime stringifies numbers (from_js in js_valkey_functions.rs:70), and SELECT replies with the simple string OK, the same reply rename() and set() already type as "OK".
  • pubsub(...): one overload per subcommand whose reply shape is fixed, typed from the replies observed against Redis 8.0.2 (CHANNELS and SHARDCHANNELS give string[], NUMSUB and SHARDNUMSUB give a flat (string | number)[] alternating name and count, NUMPAT gives a number), plus pubsub(subcommand: string, ...args: string[]): Promise<any> for everything else (HELP, lowercase spellings, future subcommands), mirroring the Promise<any> that send() already uses for untyped replies.
  • Optional trailing arguments (CHANNELS [pattern]) are separate overloads rather than pattern?: string, matching the rest of the file: cmd_strings_varargs! throws ERR_INVALID_ARG_TYPE on an explicit undefined, and an optional parameter would accept it.
  • The JSDoc carries the behavior that is not obvious from the signatures and was verified by running it: pubsub() may be called while in subscriber mode (it is registered with DontCare), select() may not (NotSubscriber), and a database chosen with select() is lost on automatic reconnect while one given in the URL path is re-selected. The subscribe() JSDoc list of commands usable while subscribed gains pubsub() so it matches the new declaration.
  • Tests: test/integration/bun-types/fixture/redis.ts asserts the exact return type of every overload with expectType(...).is<...>() and uses @ts-expect-error for the call shapes the runtime rejects (no subcommand, explicit undefined, extra argument to select). The fixture is already covered in CI by the LanguageService cases (this file runs in the bun-types GitHub workflow on a release Bun). bun-types.test.ts additionally gets a RedisClient case that type-checks that fixture with spawned tsc, sharing a helper with the existing Bun.mmap case, so the assertions also run under bun bd test, where the LanguageService cases are skipped; it is the same shape as the Bun.mmap case from types: add offset and size options to Bun.mmap #34573 and can go once the file checks the whole fixture on debug builds (tracked separately).
  • bun test test/integration/bun-types/bun-types.test.ts with the system Bun: 10 of 16 cases fail without the redis.d.ts change (15 TS2339 diagnostics, under both the DOM and no-DOM configs), 16 pass with it.
  • bun bd test test/integration/bun-types/bun-types.test.ts: the new RedisClient case fails without the redis.d.ts change and passes with it.

Background

  • valkey.classes.ts is the list of methods generated onto RedisClient.prototype; redis.d.ts is maintained by hand, so a method can exist at runtime without a declaration.
  • cmd_strings_varargs! is the macro behind both methods: it forwards every argument to the server as a string or buffer (numbers are stringified first) and throws ERR_INVALID_ARG_TYPE for anything else, including undefined. Argument count and subcommand validity are left to the server.
  • Subscriber mode is the state a client enters after subscribe(). Each method is registered as NotSubscriber (rejected in that state) or DontCare (allowed); pubsub is DontCare, select is NotSubscriber.
  • bun-types.test.ts type-checks the fixture/ directory with the in-process TypeScript LanguageService, but those cases are skipIf(isDebug) because that is very slow on debug builds; Buildkite does not run this file at all (.buildkite/ci.mjs excludes integration/bun-types), only the GitHub workflow does. Spawning tsc over a couple of files is the existing workaround for checks that should also run under a debug build.
Runtime probe (system Bun 1.4.0, local Redis 8.0.2)
pubsub("CHANNELS")                 -> ["probe-chan"]
pubsub("CHANNELS", "probe-*")      -> ["probe-chan"]
pubsub("channels")                 -> ["probe-chan"]
pubsub("NUMSUB")                   -> []
pubsub("NUMSUB", "probe-chan", "nobody") -> ["probe-chan", 1, "nobody", 0]
pubsub("NUMPAT")                   -> 2      (three psubscribe clients on two distinct patterns)
pubsub("SHARDCHANNELS")            -> ["probe-shard"]
pubsub("SHARDNUMSUB", "probe-shard", "none") -> ["probe-shard", 1, "none", 0]
pubsub("HELP")                     -> string[] (14 lines)
pubsub()                           -> rejects: ERR wrong number of arguments for 'pubsub' command
pubsub("CHANNELS", undefined)      -> throws ERR_INVALID_ARG_TYPE
subscriber.pubsub("NUMSUB", "probe-chan") -> ["probe-chan", 1]
subscriber.select(0)               -> ERR_REDIS_INVALID_STATE: RedisClient.prototype.select cannot be called while in subscriber mode.

select(1)                          -> "OK"   (a key set afterwards is visible from database 1 only)
select("2")                        -> "OK"
select()                           -> rejects: ERR wrong number of arguments for 'select' command
select(undefined)                  -> throws ERR_INVALID_ARG_TYPE
select(1, "extra")                 -> rejects: ERR wrong number of arguments for 'select' command
select(999)                        -> rejects: ERR DB index is out of range

after select(1) and a CLIENT KILL of the connection, the reconnected client writes to database 0;
a client created with redis://127.0.0.1:6379/1 writes to database 1 again after the same kill;
duplicate() of a select(2)-ed client starts on database 0.

Both methods have been registered on the prototype since Bun.redis was
introduced, but redis.d.ts never declared them, so TypeScript rejected
calls that work at runtime.

pubsub() gets one overload per PUBSUB subcommand whose reply shape is
fixed (CHANNELS, NUMSUB, NUMPAT, SHARDCHANNELS, SHARDNUMSUB) plus an
untyped catch-all; select() takes the index as a number or string, which
is what the runtime coerces.

The redis fixture gains assertions for both, and bun-types.test.ts now
type-checks that fixture with tsc so the assertions also run on debug
builds, where the LanguageService cases are skipped.
@robobun
robobun requested a review from alii as a code owner August 15, 2026 20:06
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a85ce8ad-62c0-4010-ab62-b233c01c59ba

📥 Commits

Reviewing files that changed from the base of the PR and between cc53961 and 551d0ca.

📒 Files selected for processing (3)
  • packages/bun-types/redis.d.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/redis.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:09 PM PT - Aug 15th, 2026

@robobun, your commit 551d0ca696f1bc1381b05d7a6cab58df935cfea9 passed in Build #98655! 🎉


🧪   To try this PR locally:

bunx bun-pr 39208

That installs a local version of the PR into your bun-39208 executable, so you can run:

bun-39208 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Since packages/bun-types/ has a CODEOWNER, leaving final sign-off on the overload shapes and JSDoc wording to them.

What was reviewed:

  • Confirmed pubsub and select are registered on the prototype (valkey.classes.ts:568,580) via cmd_strings_varargs! with the stated NotSubscriber/DontCare flags.
  • Return types match the RESP replies and existing conventions (Promise<"OK"> like rename(), Promise<any> fallback like send()); separate overloads instead of pattern?: string correctly reflect that the runtime rejects explicit undefined.
  • The bun-types.test.ts refactor is a behavior-preserving extraction of the existing Bun.mmap tsc check into expectTscToAccept, reused for the new fixture.
Extended reasoning...

Overview

This PR adds TypeScript declarations for two RedisClient methods that already exist at runtime but were never declared: select(index) and pubsub(subcommand, ...). It touches three files: packages/bun-types/redis.d.ts (the declarations plus a one-line addition to the subscribe() JSDoc), test/integration/bun-types/fixture/redis.ts (type-level assertions for every overload and @ts-expect-error cases), and test/integration/bun-types/bun-types.test.ts (extracts the existing Bun.mmap tsc-spawn check into a reusable expectTscToAccept helper and adds a second caller for the redis fixture).

Security risks

None. This is a .d.ts-only change with a compile-time test; no runtime code paths are touched.

Level of scrutiny

Low-to-medium. The declarations themselves are mechanical and well-researched — I verified against src/runtime/valkey_jsc/valkey.classes.ts and js_valkey_functions.rs that both methods exist, that select is NotSubscriber and pubsub is DontCare, and that cmd_strings_varargs! stringifies numbers and rejects undefined (justifying number | string for select and separate overloads over optional params). The return types ("OK", string[], (string | number)[], number, any) match the Redis PUBSUB/SELECT reply shapes and follow the file's existing conventions (rename() returns Promise<"OK">, send() returns Promise<any>). The test refactor is a straight extraction with no behavior change to the mmap case.

Other factors

packages/bun-types/ is CODEOWNER'd, and per the approval guidelines that means a human should sign off rather than auto-approving. The overload design (uppercase-literal subcommands with a string catch-all, JSDoc noting the reconnect caveat for select()) is user-facing API surface where the owner may have preferences on shape or wording. Nothing in the change looks wrong; deferring solely on the CODEOWNER rule.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review (CI green: the bun-types workflow that runs this test file passed, and Buildkite build 98655 passed).

Reproduced with the released Bun 1.4.0: pubsub() and select() succeed at runtime against a local Redis (probe output in the PR description), while tsc rejects the same calls with TS2339: Property 'pubsub' does not exist on type 'RedisClient' (same for select). Without the redis.d.ts change, test/integration/bun-types/bun-types.test.ts fails (15 such diagnostics from the updated fixture/redis.ts, both in the LanguageService cases and in the new tsc-spawning RedisClient case that also runs under bun bd test); with it, all 16 cases pass.

Self-review done. No findings against the declarations or JSDoc. It did note that the bun-types.test.ts hunk adds no CI coverage (CI already type-checks the fixture on a release Bun); it is there so the fixture assertions also run under a debug build, same as the existing Bun.mmap case. Making this file check the whole fixture on debug builds once, and adding a drift check between valkey.classes.ts and redis.d.ts (3 more undeclared methods remain, owned by #29339 and #35521), are being handled as separate follow-ups. Happy to drop the test-file hunk from this PR if you would rather wait for that.

alii pushed a commit that referenced this pull request Aug 16, 2026
…s tables (#39271)

### Problem
- `class RedisClient` in `packages/bun-types/redis.d.ts` is a
hand-written copy of the `proto` table in
`src/runtime/valkey_jsc/valkey.classes.ts`, and nothing compares the
two. A command added to the table and not to the d.ts works at runtime
and fails to type-check (`Property 'pubsub' does not exist on type
'RedisClient'`).
- On main, 5 of the 167 table entries are undeclared: `psubscribe`,
`pubsub`, `punsubscribe`, `script`, `select`. All five have been
registered since the commit that introduced `Bun.redis` (ec87a27,
#18812) and stayed undeclared through the 14 redis.d.ts commits since,
including the 614-line batch in #23116, so the gap is systematic rather
than a one-off.
- Each of the five is already being declared by an open PR: `pubsub` and
`select` by #39208, `script` by #29339, `psubscribe`/`punsubscribe` by
#35521 (which also adds the listener routing they are missing; today
`psubscribe()` drops every pmessage). This PR adds the check, not the
declarations.

### Fix
- `test/internal/source-lints/redis-client-types.test.ts` imports
`valkey.classes.ts` (the module the codegen reads;
`class-definitions.ts` has no dependencies), collects the members the
codegen installs (`proto` entries by name, `klass` entries as `static
name`, `constructor` when `construct` is set, skipping
`internal`/`privateSymbol`/`publicSymbol` entries, which the codegen
does not install under an identifier), parses the member names declared
in the `class RedisClient` body of `redis.d.ts` (`[Symbol.x]` maps onto
the table's `@@x` spelling), and requires the two sets to be equal. Each
direction is its own test, so a table entry without a declaration and a
declaration without a table entry are both reported by name.
- The five names missing today sit in a `pendingDeclarations` table
keyed by the PR that declares each. A fourth test fails as soon as a
listed name is declared or unregistered, so the entry gets deleted when
its PR lands (whichever of this PR and #39208/#29339/#35521 lands second
trips it on rebase, and the message says which entry to delete). A name
that is neither declared nor listed fails the lint outright.
- Declaration lines the parser does not recognize are reported as
failures rather than skipped, so a new d.ts shape cannot silently hide a
member from the comparison.
- `.github/workflows/source-lints.yml` gains `src/**/*.classes.ts`,
`src/codegen/class-definitions.ts` and `packages/bun-types/redis.d.ts`
as triggers: the workflow is path-filtered and those are the files this
lint reads, so without them the edits it guards would not run it. The
directory README now states that rule; a comment on the `proto` table
points at the lint.
- Scope is RedisClient only. A checker for every `*.classes.ts` needs a
class-to-declaration mapping and inheritance handling and is a separate
project.
- Verified:
- `bun test test/internal/source-lints/redis-client-types.test.ts`
passes on this branch (4 tests); the whole directory is 170 green.
- With `pendingDeclarations` emptied, the lint fails against main's
files naming exactly `psubscribe`, `pubsub`, `punsubscribe`, `script`,
`select` (output below).
- Simulated the eight drift shapes in the details block (new table
entry, pending name declared, phantom declaration, pending name
unregistered, unparseable member, `@@asyncDispose` + a `klass` entry
with and without declarations, `internal`/`privateSymbol` entries); each
fails the intended test or passes as intended.
- No runtime code changes: the `valkey.classes.ts` hunk is a comment and
produces identical codegen output. This PR has no src/packages diff for
a fail-before run to strip; the fail-before evidence is the
emptied-pending-table run above.

### Background
- `*.classes.ts` files are the input of
`src/codegen/generate-classes.ts`. `define({ proto, klass, construct })`
describes a native class: `proto` entries become properties of the
prototype (`fn` methods, `getter`/`setter` accessors), `klass` entries
become statics, and `construct: true` makes it newable. Entries with
`internal`, `privateSymbol` or `publicSymbol` are installed under
private names or `Symbol.for()` symbols (or not at all), and keys
spelled `@@x` are installed under the well-known symbol `Symbol.x`.
- `packages/bun-types` is the published `@types/bun` surface; it is
hand-written, not generated from the class definitions, which is why it
can drift.
- `test/internal/source-lints/` holds tests that only read the source
tree; `.buildkite/ci.mjs` excludes the directory from the binary lanes
and `source-lints.yml` runs it against a released bun on a bare
checkout, so tests there may only import built-ins and relative paths.

<details>
<summary>Lint output against main's files with the pending table
emptied, and the simulated drift shapes</summary>

```
(pass) every member of class RedisClient in packages/bun-types/redis.d.ts has a shape this lint can read
(fail) packages/bun-types/redis.d.ts declares every RedisClient member src/runtime/valkey_jsc/valkey.classes.ts installs
    - []
    + [
    +   "psubscribe",
    +   "pubsub",
    +   "punsubscribe",
    +   "script",
    +   "select",
    + ]
(pass) packages/bun-types/redis.d.ts declares no RedisClient member src/runtime/valkey_jsc/valkey.classes.ts does not install
(pass) pendingDeclarations lists only members that are still registered and still undeclared
```

```
### proto gains waitaof, d.ts untouched
    +   "waitaof",
    (fail) packages/bun-types/redis.d.ts declares every RedisClient member src/runtime/valkey_jsc/valkey.classes.ts installs
### d.ts declares select while it is still pending
    +   "select (#39208) is declared in packages/bun-types/redis.d.ts now; delete its entry",
    (fail) pendingDeclarations lists only members that are still registered and still undeclared
### d.ts declares flushall, which is not registered
    +   "flushall",
    (fail) packages/bun-types/redis.d.ts declares no RedisClient member src/runtime/valkey_jsc/valkey.classes.ts does not install
### proto drops script while it is still pending
    +   "script (#29339) is no longer registered in src/runtime/valkey_jsc/valkey.classes.ts",
    (fail) pendingDeclarations lists only members that are still registered and still undeclared
### d.ts gains `private brand: never;`
    +   "private brand: never;",
    (fail) every member of class RedisClient in packages/bun-types/redis.d.ts has a shape this lint can read
### proto gains "@@asyncDispose" and a klass entry, d.ts declares [Symbol.asyncDispose]() and the static
    4 pass
### same, with nothing declared
    +   "@@asyncDispose",
    +   "static parseURL",
    (fail) packages/bun-types/redis.d.ts declares every RedisClient member src/runtime/valkey_jsc/valkey.classes.ts installs
### proto gains an `internal: true` entry and a `privateSymbol` entry, d.ts untouched
    4 pass
```

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant