diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 9f496436e0ed..4a6df0711ab3 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -2676,6 +2676,29 @@ declare module "bun" { */ ping(message: RedisClient.KeyLike): Promise; + /** + * Switch this connection to another logical database + * + * A connection starts on the database named by the path of the connection + * URL (`redis://localhost:6379/2` selects database 2), or on database 0 + * when the URL has no path. That URL database is selected again after an + * automatic reconnect and is what {@link duplicate `.duplicate()`} starts + * on; a database chosen with this method only lasts until the connection + * drops. Not available while the client is in subscriber mode. + * + * @param index The database index. Servers have 16 databases (0 to 15) + * unless configured otherwise. + * @returns Promise that resolves with "OK", or rejects when the index is + * out of range for the server + * + * @example + * ```ts + * await redis.select(1); + * await redis.set("key", "value"); // written to database 1 + * ``` + */ + select(index: number | string): Promise<"OK">; + /** * Publish a message to a Redis channel. * @@ -2695,8 +2718,9 @@ declare module "bun" { * * Subscribing moves the channel to a dedicated subscription state which * prevents most other commands from being executed until unsubscribed. Only - * {@link ping `.ping()`}, {@link subscribe `.subscribe()`}, and - * {@link unsubscribe `.unsubscribe()`} can be called while subscribed. + * {@link ping `.ping()`}, {@link subscribe `.subscribe()`}, + * {@link unsubscribe `.unsubscribe()`}, and {@link pubsub `.pubsub()`} can + * be called while subscribed. * * @param channel The channel to subscribe to. * @param listener The listener to call when a message is received on the @@ -2793,6 +2817,114 @@ declare module "bun" { */ duplicate(): Promise; + /** + * List the channels that currently have at least one subscriber + * + * The result covers every client connected to the server, not only this + * one, and only counts subscriptions made with + * {@link subscribe `.subscribe()`}: pattern subscriptions are not included. + * Unlike most commands, PUBSUB can also be sent while this client is in + * subscriber mode. + * + * @param subcommand "CHANNELS" + * @returns Promise that resolves with the channel names + * + * @example + * ```ts + * await subscriber.subscribe("news", () => {}); + * console.log(await redis.pubsub("CHANNELS")); // ["news"] + * ``` + */ + pubsub(subcommand: "CHANNELS"): Promise; + + /** + * List the channels that currently have at least one subscriber and whose + * name matches a pattern + * + * See the single-argument overload for what is counted. + * + * @param subcommand "CHANNELS" + * @param pattern Glob-style pattern, as used by `KEYS` (`"news.*"`) + * @returns Promise that resolves with the matching channel names + */ + pubsub(subcommand: "CHANNELS", pattern: string): Promise; + + /** + * Count the subscribers of each given channel + * + * Counts the subscriptions every client connected to the server made with + * {@link subscribe `.subscribe()`}; pattern subscriptions are not included. + * A channel nobody is subscribed to is reported with a count of 0. + * + * @param subcommand "NUMSUB" + * @param channels The channels to count subscribers for + * @returns Promise that resolves with a flat array alternating channel name + * and subscriber count, in the order the channels were given (empty when no + * channels were given) + * + * @example + * ```ts + * await subscriber.subscribe("news", () => {}); + * console.log(await redis.pubsub("NUMSUB", "news", "sports")); // ["news", 1, "sports", 0] + * ``` + */ + pubsub(subcommand: "NUMSUB", ...channels: string[]): Promise<(string | number)[]>; + + /** + * Count the patterns clients are subscribed to + * + * @param subcommand "NUMPAT" + * @returns Promise that resolves with the number of distinct patterns any + * client connected to the server is subscribed to. Two clients subscribed + * to the same pattern count once. + */ + pubsub(subcommand: "NUMPAT"): Promise; + + /** + * List the shard channels that currently have at least one subscriber + * + * Shard channels are the ones used by `SSUBSCRIBE` and + * {@link spublish `.spublish()`}; regular channels are not included. + * + * @param subcommand "SHARDCHANNELS" + * @returns Promise that resolves with the shard channel names + */ + pubsub(subcommand: "SHARDCHANNELS"): Promise; + + /** + * List the shard channels that currently have at least one subscriber and + * whose name matches a pattern + * + * @param subcommand "SHARDCHANNELS" + * @param pattern Glob-style pattern, as used by `KEYS` + * @returns Promise that resolves with the matching shard channel names + */ + pubsub(subcommand: "SHARDCHANNELS", pattern: string): Promise; + + /** + * Count the subscribers of each given shard channel + * + * @param subcommand "SHARDNUMSUB" + * @param shardchannels The shard channels to count subscribers for + * @returns Promise that resolves with a flat array alternating shard channel + * name and subscriber count, in the order the channels were given (empty + * when no channels were given) + */ + pubsub(subcommand: "SHARDNUMSUB", ...shardchannels: string[]): Promise<(string | number)[]>; + + /** + * Send any other PUBSUB subcommand + * + * The subcommand and its arguments are sent to the server as given. The + * server also accepts lowercase spellings of the subcommands above; they + * go through this overload, so their replies are untyped. + * + * @param subcommand The subcommand, for example `"HELP"` + * @param args The subcommand's arguments + * @returns Promise that resolves with the server's reply + */ + pubsub(subcommand: string, ...args: string[]): Promise; + /** * Copy the value stored at the source key to the destination key * diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..d84c005d675f 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -367,36 +367,51 @@ describe("@types/bun integration test", () => { }); }); - // Runs on debug builds too: spawning tsc over a single file is cheap, - // unlike the in-process LanguageService runs above. + // The checks below run on debug builds too: spawning tsc over a few files is + // cheap, unlike the in-process LanguageService runs above. + async function expectTscToAccept(checkDirName: string, files: Record) { + const checkDir = join(TEMP_DIR, checkDirName); + const tsconfig = structuredClone(sourceTsconfig); + tsconfig.include = Object.keys(files); + tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; + await mkdir(checkDir, { recursive: true }); + await makeTree(checkDir, { + "tsconfig.json": JSON.stringify(tsconfig, null, 2), + ...files, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], + env: bunEnv, + cwd: checkDir, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr.trim()).toBe(""); + expect(stdout.trim()).toBe(""); + expect(exitCode).toBe(0); + } + describe("Bun.mmap", () => { test("MMapOptions accepts offset and size", async () => { - const checkDir = join(TEMP_DIR, "mmap-options-check"); - const tsconfig = structuredClone(sourceTsconfig); - tsconfig.include = ["mmap-options.ts"]; - tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; - await mkdir(checkDir, { recursive: true }); - await makeTree(checkDir, { - "tsconfig.json": JSON.stringify(tsconfig, null, 2), + await expectTscToAccept("mmap-options-check", { "mmap-options.ts": `const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 }); view satisfies Uint8Array; Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array; Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array;`, }); + }); + }); - await using proc = Bun.spawn({ - cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], - env: bunEnv, - cwd: checkDir, - stdout: "pipe", - stderr: "pipe", + describe("RedisClient", () => { + test("the redis fixture type-checks (pubsub and select overloads)", async () => { + await expectTscToAccept("redis-fixture-check", { + "utilities.ts": readFileSync(join(FIXTURE_SOURCE_DIR, "utilities.ts"), "utf8"), + "redis.ts": readFileSync(join(FIXTURE_SOURCE_DIR, "redis.ts"), "utf8"), }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stderr.trim()).toBe(""); - expect(stdout.trim()).toBe(""); - expect(exitCode).toBe(0); }); }); diff --git a/test/integration/bun-types/fixture/redis.ts b/test/integration/bun-types/fixture/redis.ts index f7e536200d63..7a8cebe21385 100644 --- a/test/integration/bun-types/fixture/redis.ts +++ b/test/integration/bun-types/fixture/redis.ts @@ -29,3 +29,34 @@ await copy.unsubscribe(); await copy.unsubscribe("hello"); expectType(copy.unsubscribe("hello", () => {})).is>(); + +// The PUBSUB subcommands the runtime is known to forward get typed replies; +// anything else (including lowercase spellings) goes through the untyped overload. +expectType(Bun.redis.pubsub("CHANNELS")).is>(); +expectType(Bun.redis.pubsub("CHANNELS", "news.*")).is>(); +expectType(Bun.redis.pubsub("NUMSUB")).is>(); +expectType(Bun.redis.pubsub("NUMSUB", "news", "sports")).is>(); +expectType(Bun.redis.pubsub("NUMPAT")).is>(); +expectType(Bun.redis.pubsub("SHARDCHANNELS")).is>(); +expectType(Bun.redis.pubsub("SHARDCHANNELS", "orders-*")).is>(); +expectType(Bun.redis.pubsub("SHARDNUMSUB")).is>(); +expectType(Bun.redis.pubsub("SHARDNUMSUB", "orders", "payments")).is>(); +expectType(Bun.redis.pubsub("HELP")).is>(); +expectType(Bun.redis.pubsub("channels", "news.*")).is>(); +expectType(copy.pubsub("NUMPAT")).is>(); + +// @ts-expect-error a subcommand is required +Bun.redis.pubsub(); +// @ts-expect-error the runtime rejects undefined arguments instead of skipping them +Bun.redis.pubsub("CHANNELS", undefined); + +expectType(Bun.redis.select(1)).is>(); +expectType(Bun.redis.select("1")).is>(); +expectType(copy.select(2)).is>(); + +// @ts-expect-error the database index is required +Bun.redis.select(); +// @ts-expect-error the runtime rejects undefined arguments instead of skipping them +Bun.redis.select(undefined); +// @ts-expect-error SELECT takes exactly one argument +Bun.redis.select(1, "extra");