Skip to content

Redis PUB/SUB - #21728

Merged
markovejnovic merged 15 commits into
oven-sh:mainfrom
markovejnovic:user/marko/add-redis-pub-sub
Sep 10, 2025
Merged

markovejnovic merged 15 commits into
oven-sh:mainfrom
markovejnovic:user/marko/add-redis-pub-sub

Conversation

@markovejnovic

@markovejnovic markovejnovic commented Aug 10, 2025 •

Copy link
Copy Markdown
Contributor

What does this PR do?

The goal of this PR is to introduce PUB/SUB functionality to the built-in Redis client. Based on the fact that the current Redis API does not appear to have compatibility with io-redis or redis-node, I've decided to do away with existing APIs and API compatibility with these existing libraries.

I have decided to base my implementation on the redis-node pub/sub API.

How did you verify your code works?

I've written a set of unit tests to hopefully catch the major use-cases of this feature. They all appear to pass:

image

Future Improvements

I would have a lot more confidence in our Redis implementation if we tested it with a test suite running over a network which emulates a high network failure rate. There are large amounts of edge cases that are worthwhile to grab, but I think we can roll that out in a future PR.

Future Tasks

  • Tests over flaky network
  • Use the custom private members over _<member>.

@gerardatkonvo

Copy link
Copy Markdown

Really interested in seeing this move forward

@markovejnovic markovejnovic changed the title [Tentative Redis Pub Sub] Redis PUB/SUB Sep 1, 2025
@markovejnovic
markovejnovic marked this pull request as ready for review September 1, 2025 05:37
@coderabbitai

coderabbitai Bot commented Sep 1, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Redis pub/sub typings and runtime subscription support: introduces per-client SubscriptionCtx, subscriber-mode guards and wrappers, duplicate() support, JSMap get with exception handling and size, minor runtime/debugging/logging changes, tests and TypeScript typing coverage.

Changes

Cohort / File(s) Summary of changes
Redis typings
packages/bun-types/redis.d.ts
Adds StringPubSubListener type; adds publish, subscribe/unsubscribe overloads; adds duplicate(): Promise<RedisClient>; adds connected, bufferedAmount, onconnect, and ping(message) overload (typings-only).
Valkey subscription subsystem
src/valkey/...
src/valkey/js_valkey.zig, src/valkey/js_valkey_functions.zig, src/valkey/valkey.zig, src/valkey/ValkeyCommand.zig
Introduces SubscriptionCtx and per-channel callback storage; adds _subscription_ctx, channel_subscription_count, has_pending_activity; adds subscriber-mode guards and wrapper APIs for publish/subscribe/unsubscribe/psubscribe/punsubscribe; adds duplicate export and bridging methods (onValkeySubscribe, onValkeyUnsubscribe, onValkeyMessage); updates Promise.reject typing to use JSError!JSValue.
JSMap bindings & exports
src/bun.js/bindings/JSMap.zig, src/bun.js/bindings/bindings.cpp, src/bun.js/bindings/headers.h
Renames exported symbol JSC__JSMap__get_ → JSC__JSMap__get; JSMap.get now returns bun.JSError!?JSValue and uses catch-scope exception handling; adds JSMap.size Zig API and C++ export JSC__JSMap__size; updates headers and includes.
Runtime proto & class mapping
src/bun.js/api/valkey.classes.ts
Adds hasPendingActivity: true to RedisClient public definition; inserts duplicate proto method mapping; appends subscriptionCallbackMap to proto values.
Valkey JS runtime wrappers
src/bun.js/api/valkey.classes.ts, src/valkey/js_valkey_functions.zig
Converts many command exports to guarded wrapper functions enforcing subscriber-mode checks; implements publish/subscribe/unsubscribe wrapper logic and duplicate wrapper export; adds requireSubscriber/requireNotSubscriber guards.
uWebSockets debug formatting
src/deps/uws/us_socket_t.zig
Changes debug logging to format socket pointers with {p} and pass pointer args directly; no API behavior changes.
StringBuilder memory transfer
src/string/StringBuilder.zig
Adds pub fn moveToSlice(this: *StringBuilder, into_slice: *[]u8) void to transfer ownership of the allocated slice to the caller and reset the builder.
Valkey tests & test utilities
test/js/valkey/test-utils.ts, test/js/valkey/valkey.test.ts, test/integration/bun-types/fixture/redis.ts
Documents restartRedisContainer; adds randomCoinFlip() export; introduces comprehensive PUB/SUB and duplicate() tests; adds TypeScript typing integration test for Redis pub/sub and duplicate typings.

Suggested reviewers

  • Jarred-Sumner

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Redis PUB/SUB” directly highlights the primary change in this pull request, namely the introduction of publish/subscribe functionality to the built-in Redis client. It is concise, avoids extraneous detail, and accurately reflects the main feature added without listing every modified component. A reviewer scanning the commit history can immediately understand that this PR implements Redis Pub/Sub support. Although it is phrased as a fragment, it remains clear and focused on the core enhancement.
Description Check ✅ Passed The current description includes both required sections from the repository template—“What does this PR do?” and “How did you verify your code works?”—and provides clear context on the implementation approach and the unit tests used for validation. All mandatory headings are present and adequately filled out, and the additional “Future Improvements” and “Future Tasks” sections are non-critical extras that do not impede clarity or completeness. Thus, the description meets the template requirements and sufficiently explains the PR’s intent and verification.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 505fcba and cf8a39e.

📒 Files selected for processing (1)
  • test/js/valkey/test-utils.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/js/valkey/test-utils.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/valkey/js_valkey.zig (1)

721-729: Unbalanced refcount in onValkeyClose (extra deref without ref).

There’s a top-level defer this.deref() without a matching ref(). This will underflow the refcount.

Apply this diff:

 pub fn onValkeyClose(this: *JSValkeyClient) void {
     const globalObject = this.globalObject;
     this.poll_ref.disable();
-    defer this.deref();
 
     const this_jsvalue = this.this_value.tryGet() orelse return;
     this.this_value.setWeak(this_jsvalue);
     this.ref();
     defer this.deref();
test/js/valkey/test-utils.ts (1)

203-205: Fix invalid redis+unix URL (missing //).

This will produce an invalid socket URL when reusing a running container.

-        UNIX_REDIS_URL = `redis+unix:${REDIS_UNIX_SOCKET}`;
+        UNIX_REDIS_URL = `redis+unix://${REDIS_UNIX_SOCKET}`;
src/bun.js/bindings/JSMap.zig (1)

16-22: Map.get optional semantics are broken; .zero won’t detect “missing” and conflates undefined.

JSMap.get_ returns undefined when key is absent. Comparing to .zero won’t catch that. Return null only when key is absent (has==false); otherwise return the actual value (including undefined).

-    pub fn get(this: *JSMap, globalObject: *JSGlobalObject, key: JSValue) ?JSValue {
-        const value = get_(this, globalObject, key);
-        if (value == .zero) {
-            return null;
-        }
-        return value;
-    }
+    pub fn get(this: *JSMap, globalObject: *JSGlobalObject, key: JSValue) ?JSValue {
+        const value = get_(this, globalObject, key);
+        // If the stored value is `undefined`, we must check membership.
+        if (value.isUndefined()) {
+            if (!has(this, globalObject, key)) return null;
+        }
+        return value;
+    }
🧹 Nitpick comments (19)
src/deps/uws/us_socket_t.zig (1)

135-140: Make writeFd log consistent with {p} pointer formatting.

Align this debug with the new style.

Apply this diff:

-        debug("us_socket_ipc_write_fd({d}, {d}, {d}) = {d}", .{ @intFromPtr(this), data.len, file_descriptor.native(), rc });
+        debug("us_socket_ipc_write_fd({p}, {d}, {d}) = {d}", .{ this, data.len, file_descriptor.native(), rc });

Optionally, do the same for open/pause/resume/close/shutdown debug lines for uniformity.

src/valkey/valkey.zig (5)

360-366: Don’t log raw payload bytes; log length (and gate in debug) to avoid PII/leaks and large allocations.

Current {s} will dump arbitrary bytes.

Apply this diff:

-        this.debug("Sending payload {s}", .{chunk});
+        if (comptime bun.Environment.allow_assert) this.debug("Sending payload (len={d})", .{chunk.len});

394-413: Typo and readability: rename deferred_failrue → deferred_failure.

Minor but helps future maintenance.

Apply this diff:

-                const deferred_failrue = bun.new(DeferredFailure, .{
+                const deferred_failure = bun.new(DeferredFailure, .{
@@
-                deferred_failrue.enqueue();
+                deferred_failure.enqueue();

446-472: Consider jitter in reconnect delay to avoid thundering herd.

Add a small random jitter (+/- 10–20%) when logging/scheduling reconnection.


719-721: Verbose per-message dump; consider gating.

onData() {any} can be very chatty under load; gate with allow_assert or lower verbosity.


1087-1087: Align scoped logger with project convention.

Use false for visibility by default to match “scoped(.${SCOPE}, false)”.

Apply this diff:

-const scopedDebug = bun.Output.scoped(.Redis, .visible);
+const scopedDebug = bun.Output.scoped(.Redis, false);
src/valkey/js_valkey.zig (3)

1-132: SubscriptionCtx API is solid; two small polish items.

  • invokeCallback: include the channel in “No callbacks found” to aid debugging.
  • The comment “mandatory to guard deinit with hasSubscriptions” isn’t enforced in deleteSubscriptionCtx/finalize; either soften the wording or guard where applicable.

Apply this diff (first item):

-            scopedDebug("No callbacks found for channel", .{});
+            scopedDebug("No callbacks found for channel", .{});
+            // Optional: print channel name if inexpensive to stringify

809-815: Nit: use “Valkey” in logs for consistency.

Minor wording.

Apply this diff:

-        this.debug("Connecting to Redis.", .{});
+        this.debug("Connecting to Valkey.", .{});

1140-1140: Align logger default visibility with convention.

Match “scoped(.${SCOPE}, false)” to reduce default noise; enable via env when needed.

Apply this diff:

-const scopedDebug = bun.Output.scoped(.RedisJS, .visible);
+const scopedDebug = bun.Output.scoped(.RedisJS, false);
test/js/valkey/test-utils.ts (1)

786-789: Avoid randomness in tests; use deterministic inputs.

Randomness makes failures non-reproducible. Prefer table-driven cases or a seed derived from context/test name. If you keep a coin flip, simplify it.

-export function randomCoinFlip(): boolean {
-  return Math.floor(Math.random() * 2) == 0;
-}
+export function randomCoinFlip(): boolean {
+  return Math.random() < 0.5;
+}

Optionally: replace callers with test.each([...channels]) to cover both paths deterministically.

src/bun.js/api/valkey.classes.ts (1)

230-231: Validate exposing subscriptionCtx as a public value.

If this is internal state, omit it from values. If it should be public and cached, prefer a getter with cache: true per the codegen rules.

src/bun.js/bindings/headers.h (1)

132-132: Nit: run clang-format; stray double-space in signature.

There’s an extra space before uint8_t in this declaration. Please format the header per repo rules.

-CPP_DECL WebCore::AbortSignal* WebCore__AbortSignal__signal(WebCore::AbortSignal* arg0, JSC::JSGlobalObject*,  uint8_t abortReason);
+CPP_DECL WebCore::AbortSignal* WebCore__AbortSignal__signal(WebCore::AbortSignal* arg0, JSC::JSGlobalObject*, uint8_t abortReason);
test/js/valkey/valkey.test.ts (2)

232-241: Use rejects for async API errors (subscriber mode).

set() returns a Promise; asserting sync throws is brittle. Prefer rejects.toThrow(...).

-      expect(() => redis.set(testKey, testValue)).toThrow("Cannot use in subscriber mode");
+      await expect(redis.set(testKey, testValue)).rejects.toThrow("Cannot use in subscriber mode");

If set() genuinely throws synchronously in your binding, consider still returning a rejected Promise for API consistency.


552-556: Remove noisy console logs in tests.

These add noise to CI output.

-      console.log("Publishing messages to channel with error callback");
...
-      console.log("Unsubscribing from channel after error test");

Also applies to: 562-564

packages/bun-types/redis.d.ts (3)

659-666: JSDoc: fix typos and allowed-commands list for subscriber mode.

Typos (“pipeling”, “@subscribe”, “sunsunscribe”, “ssubscribe”) and incorrect/unclear list of allowed commands.

-     * @note Subscribing disables automatic pipeling, so all commands will be
-     *       received immediately.
-     * @note Subscribing moves the channel to a dedicated subscription state
-     *       which prevents most other commands from being executed until
-     *       unsubscribed. Only @see ping, @see psubscribe, @see punsubscribe,
-     *       @see ssubscribe, @see @subscribe, @see sunsunscribe and @see
-     *       unsubscribe are legal to invoke in a subscribed upon channel.
+     * @note Subscribing disables automatic pipelining.
+     * @note While in subscription mode, most commands are disabled. Only
+     *       PING, SUBSCRIBE/UNSUBSCRIBE, and PSUBSCRIBE/PUNSUBSCRIBE are allowed.

Optionally add psubscribe/punsubscribe to the typings to match runtime exports.


712-716: Typo in JSDoc (“@parm”).

-     * @parm channels An array of channels to unsubscribe from.
+     * @param channels An array of channels to unsubscribe from.

731-741: Clarify that on("message") uses (channel, message) arg order.

Docs are fine here; ensure it explicitly contrasts with subscribe listener order to avoid confusion.

-     * Handle messages coming from the Redis server.
+     * Handle messages coming from the Redis server.
+     * Listener receives (channel, message). Prefer `subscribe()` for per-channel handlers.
src/valkey/js_valkey_functions.zig (2)

772-865: unsubscribe(): consider mirroring async cleanup for the “all channels” branch.

You call deleteSubscriptionCtx() immediately. To avoid brief inconsistent state if UNSUBSCRIBE fails, attach a then/catch like in subscribe(). Optional but safer.

-        // Clear all subscriptions and exit subscription mode
-        this.deleteSubscriptionCtx();
-
-        return promise.toJS();
+        const pv = promise.toJS();
+        bun.cpp.JSC__JSValue__then(
+          pv,
+          globalObject,
+          onSubscribeResolve, // no-op on resolve
+          onSubscribeReject   // ensures cleanup on reject
+        );
+        // Clear local state now; remote will follow.
+        this.deleteSubscriptionCtx();
+        return pv;

731-752: subscribe(): duplicate type checks for array vs string.

You check channelOrMany twice (once for building args, once for handlers). Consolidate to avoid divergence bugs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fcaff77 and 6b53e0713e3b92c750f75c31e6d78783444309e2.

📒 Files selected for processing (11)
  • packages/bun-types/redis.d.ts (1 hunks)
  • src/bun.js/api/valkey.classes.ts (1 hunks)
  • src/bun.js/bindings/JSMap.zig (2 hunks)
  • src/bun.js/bindings/bindings.cpp (1 hunks)
  • src/bun.js/bindings/headers.h (3 hunks)
  • src/deps/uws/us_socket_t.zig (2 hunks)
  • src/valkey/js_valkey.zig (13 hunks)
  • src/valkey/js_valkey_functions.zig (20 hunks)
  • src/valkey/valkey.zig (17 hunks)
  • test/js/valkey/test-utils.ts (1 hunks)
  • test/js/valkey/valkey.test.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (17)
**/*.{cpp,cxx,cc,hpp,hxx,h}

📄 CodeRabbit inference engine (CLAUDE.md)

Format C++ sources/headers with clang-format (bun run clang-format)

Files:

  • src/bun.js/bindings/bindings.cpp
  • src/bun.js/bindings/headers.h
src/bun.js/bindings/**/*.{cpp,hpp,h}

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.{cpp,hpp,h}: When implementing JavaScript classes in C++, create Foo, FooPrototype, and FooConstructor for public constructors
Define properties using JSC HashTableValue arrays for JS class bindings
Add iso subspaces for classes with C++ fields
Cache structures in ZigGlobalObject for bound classes

Files:

  • src/bun.js/bindings/bindings.cpp
  • src/bun.js/bindings/headers.h
**/*.{cpp,h}

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.{cpp,h}: When exposing a JS class with public Constructor and Prototype, define three C++ types: class Foo : public JSC::DestructibleObject (if it has C++ fields), class FooPrototype : public JSC::JSNonFinalObject, and class FooConstructor : public JSC::InternalFunction
If the class has C++ data members, inherit from JSC::DestructibleObject and provide proper destruction; if it has no C++ fields (only JS properties), avoid a class and use JSC::constructEmptyObject(vm, structure) with putDirectOffset
Prefer placing the subspaceFor implementation in the .cpp file rather than the header when possible

Files:

  • src/bun.js/bindings/bindings.cpp
  • src/bun.js/bindings/headers.h
**/*.cpp

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.cpp: Include "root.h" at the top of C++ binding files to satisfy lints
Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros
Prototype classes should subclass JSC::JSNonFinalObject, provide create/createStructure, DECLARE_INFO, finishCreation that reifies static properties, and set mayBePrototype on the Structure
Custom getters should use JSC_DEFINE_CUSTOM_GETTER, jsDynamicCast to validate this, and throwThisTypeError on mismatch
Custom setters should use JSC_DEFINE_CUSTOM_SETTER, validate this via jsDynamicCast, and store via WriteBarrier/set semantics
Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue
Constructors should subclass JSC::InternalFunction, return internalFunctionSpace in subspaceFor, set the prototype property as non-configurable/non-writable, and provide create/createStructure
Provide a setup function that builds the Prototype, Constructor, and Structure, and assigns them to the LazyClassStructure initializer
Use the cached Structure via globalObject->m_.get(globalObject) when constructing instances
Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure
Provide an extern "C" Bun____toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Files:

  • src/bun.js/bindings/bindings.cpp
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/deps/uws/us_socket_t.zig
  • src/bun.js/bindings/JSMap.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/deps/uws/us_socket_t.zig
  • src/bun.js/bindings/JSMap.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/bindings/JSMap.zig
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript with Prettier (bun run prettier)

Files:

  • test/js/valkey/test-utils.ts
  • src/bun.js/api/valkey.classes.ts
  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
**/*.classes.ts

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

**/*.classes.ts: Define JavaScript class interfaces in .classes.ts using define({ name, constructor, JSType, finalize, proto: { ... } })
Use cache: true on properties that should use JSC WriteBarrier-backed caching

Files:

  • src/bun.js/api/valkey.classes.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Place tests in test/ and ensure filenames end with .test.ts or .test.tsx
Always use port: 0 in tests; never hardcode port numbers or use custom random-port functions
Prefer snapshot tests using normalizeBunSnapshot(...).toMatchInlineSnapshot(...) over exact string comparisons
Never write tests that assert absence of crashes (e.g., no 'panic' or 'uncaught exception' in output)
Avoid shell commands like find/grep in tests; use Bun.Glob and built-in tools instead

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Use bun:test in files that end in *.test.ts
Prefer async/await over callbacks in tests
For single-callback flows in tests, use Promise.withResolvers()
Do not set timeouts on tests; rely on Bun’s built-in timeouts
Use tempDirWithFiles from harness to create temporary directories/files in tests
Organize tests with describe blocks
Always assert exit codes and error scenarios in tests (e.g., proc.exited, expect(...).not.toBe(0), toThrow())
Use describe.each for parameterized tests; toMatchSnapshot for snapshots; and beforeAll/afterEach/beforeEach for setup/teardown
Avoid flaky tests: never wait for arbitrary time; wait for conditions to be met

Files:

  • test/js/valkey/valkey.test.ts
test/{**/*.test.ts,**/*-fixture.ts}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/{**/*.test.ts,**/*-fixture.ts}: When spawning Bun processes, use bunExe() and bunEnv from harness
Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup
Never hardcode port numbers; use port: 0 to get a random port
Use Buffer.alloc(count, fill).toString() instead of "A".repeat(count) for large/repetitive strings
Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Files:

  • test/js/valkey/valkey.test.ts
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (21)
📚 Learning: 2025-08-30T00:05:37.983Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-30T00:05:37.983Z
Learning: Applies to src/bun.js/bindings/**/*.{cpp,hpp,h} : Define properties using JSC HashTableValue arrays for JS class bindings

Applied to files:

  • src/bun.js/bindings/bindings.cpp
  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.878Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.878Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/**/*.zig : Implement getters as get<PropertyName>(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:11:57.056Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.056Z
Learning: Applies to src/{**/js_*.zig,bun.js/api/**/*.zig} : Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/**/*.zig : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/**/*.zig : Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:11:57.056Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.056Z
Learning: Applies to src/bun.js/api/BunObject.zig : Create a getter function in BunObject.zig (define const Getter with toJSGetter and export it in exportAll)

Applied to files:

  • src/bun.js/bindings/JSMap.zig
📚 Learning: 2025-08-30T00:11:00.878Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.878Z
Learning: Applies to **/*.zig : Declare the extern C symbol in Zig and export a Zig-friendly alias for use

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.878Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.878Z
Learning: Applies to **/*.cpp : Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.878Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.878Z
Learning: Applies to **/*.cpp : Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.878Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.878Z
Learning: Applies to **/*.cpp : Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:12:56.792Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.792Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.612Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.612Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.792Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.792Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.087Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.087Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.612Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.612Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : When spawning Bun processes, use bunExe() and bunEnv from harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.612Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.612Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:05:37.983Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-30T00:05:37.983Z
Learning: Applies to test/js/bun/**/*.test.{ts,tsx} : Put Bun-specific API tests under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:54.560Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.560Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (1)
test/js/valkey/test-utils.ts (1)
  • randomCoinFlip (787-789)
🪛 Biome (2.1.2)
packages/bun-types/redis.d.ts

[error] 668-668: Duplicate parameter name.

The parameter overrides a preceding parameter by using the same name.

(lint/suspicious/noDuplicateParameters)


[error] 685-685: Duplicate parameter name.

The parameter overrides a preceding parameter by using the same name.

(lint/suspicious/noDuplicateParameters)

🔇 Additional comments (10)
src/deps/uws/us_socket_t.zig (1)

131-133: Pointer-format logging change is correct.

Switching to {p} with the raw pointer is preferable and consistent with Zig’s formatter.

Also applies to: 145-150

src/valkey/valkey.zig (1)

175-183: Per-client debug helper looks good.

Provides consistent, contextual logs with identifiers.

src/valkey/js_valkey.zig (3)

319-341: Entering subscription mode correctly snapshots flags and disables queue/pipelining.

Looks good.


632-658: Subscribe/unsubscribe hooks: consider updating per-channel state here.

If js_valkey_functions.zig doesn’t already, decrement subscription count and auto-exit subscriber mode when it reaches zero.


660-705: Argument order for message callbacks — confirm API contract.

You dispatch (message, channel). If the public API expects (channel, message), invert to prevent subtle incompatibilities.

src/bun.js/bindings/JSMap.zig (1)

32-34: Confirm usize vs u32 for size() overflow semantics.

The C++ binding returns uint32_t; Zig exposes usize. If >4B entries are impossible, this is fine. Otherwise, consider u32 in Zig for clarity.

src/bun.js/bindings/bindings.cpp (1)

6419-6423: LGTM: JSMap.size binding added correctly.

Signature and export attributes align with adjacent JSMap bindings.

src/bun.js/bindings/headers.h (1)

193-193: JSMap__size linkage verified. Declaration in headers.h (line 193), single definition in bindings.cpp (line 6419), and Zig binding in JSMap.zig (line 33) all match the expected signature and [[ZIG_EXPORT(nothrow)]] ABI.

src/valkey/js_valkey_functions.zig (2)

43-46: Subscriber-mode guards look correct.

The early exits with a clear error are consistent and match tests.

Also applies to: 68-71, 92-95, 142-145, 166-169, 190-193, 215-218, 250-253, 275-278, 304-307, 329-332, 354-357, 379-382, 408-411, 438-441, 489-492, 523-526, 557-560


1408-1427: Good: host hooks for subscribe resolution/rejection.

These are appropriate; once then is attached (see above), they’ll correctly manage cleanup.

Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/valkey/valkey.zig (1)

622-667: Subscriber error path tears down the entire client; add pattern-pubsub support (‘pmessage’/‘psubscribe’/‘punsubscribe’).

  • Don’t call fail() for SUBSCRIBE/UNSUBSCRIBE command errors; reject the promise and continue. Failing here drops unrelated workloads.
  • Handle RESP3 pattern pub/sub kinds: pmessage, psubscribe, punsubscribe. Currently these hit the “Unexpected push message” path and fail() the client.
@@
-            .Error => |err| {
-                promise_ptr.reject(
-                    globalThis,
-                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
-                );
-                this.fail(err, protocol.RedisError.InvalidResponse);
-            },
+            .Error => |err| {
+                // Reject the in-flight subscribe/unsubscribe promise but do not fail the client.
+                promise_ptr.reject(
+                    globalThis,
+                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
+                );
+                this.debug("subscribe command error: {s}", .{err});
+                return;
+            },
             .Push => |push| {
-                if (std.mem.eql(u8, push.kind, "subscribe")) {
+                if (std.mem.eql(u8, push.kind, "subscribe") or std.mem.eql(u8, push.kind, "psubscribe")) {
                     this.onValkeySubscribe(value);
                     promise_ptr.resolve(globalThis, value);
                     return;
-                } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
+                } else if (std.mem.eql(u8, push.kind, "unsubscribe") or std.mem.eql(u8, push.kind, "punsubscribe")) {
                     this.onValkeyUnsubscribe(value);
                     promise_ptr.resolve(globalThis, value);
                     return;
                 } else {
@@
-                .Push => |push| {
-                    if (std.mem.eql(u8, push.kind, "message")) {
+                .Push => |push| {
+                    if (std.mem.eql(u8, push.kind, "message") or std.mem.eql(u8, push.kind, "pmessage")) {
                         @branchHint(.likely);
-                        this.debug("Received a message.", .{});
+                        this.debug("Received a message.", .{});
                         this.onValkeyMessage(push.data);
                         return;
-                    } else if (std.mem.eql(u8, push.kind, "subscribe")) {
+                    } else if (std.mem.eql(u8, push.kind, "subscribe") or std.mem.eql(u8, push.kind, "psubscribe")) {
                         @branchHint(.cold);
                         this.debug("Received subscription message without promise: {any}", .{push.data});
                         return;
-                    } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
+                    } else if (std.mem.eql(u8, push.kind, "unsubscribe") or std.mem.eql(u8, push.kind, "punsubscribe")) {
                         @branchHint(.cold);
                         this.debug("Received unsubscribe message without promise: {any}", .{push.data});
                         return;
                     } else {
                         @branchHint(.cold);
                         this.fail("Unexpected push message kind without promise", protocol.RedisError.InvalidResponseType);
                         return;
                     }
                 },
#!/bin/bash
# Expect to see handlers for 'message', 'pmessage', 'subscribe', 'psubscribe', 'unsubscribe', 'punsubscribe'
rg -nC2 -e '\bpmessage\b|\bpsubscribe\b|\bpunsubscribe\b|\bsubscribe\b|\bunsubscribe\b' src/valkey

Also applies to: 756-787

src/valkey/js_valkey_functions.zig (2)

669-671: publish(): wrong error label and overly-permissive message type.

Change the thrown op name to “publish” and restrict message to string to match typical typings.

-    const message = (try fromJS(globalObject, callframe.argument(1))) orelse {
-        return globalObject.throwInvalidArgumentType("set", "message", "string or buffer or number");
-    };
+    const message = (try fromJS(globalObject, callframe.argument(1))) orelse {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    };

If non-strings must be supported, update the .d.ts accordingly instead.


704-707: subscribe(): fix callback name in error; attach rejection handler to clean up on async failure.

  • The invalid-argument label should say “listener” and “function”.
  • If send(SUBSCRIBE) later rejects, call deleteSubscriptionCtx(); currently only immediate send errors clean up.
-    if (!handler_callback.isCallable()) {
-        return globalObject.throwInvalidArgumentType("subscribe", "message", "callable");
-    }
+    if (!handler_callback.isCallable()) {
+        return globalObject.throwInvalidArgumentType("subscribe", "listener", "function");
+    }
@@
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    // Ensure we clean up subscription state if the subscribe promise later rejects.
+    bun.cpp.JSC__JSValue__then(promise_value, globalObject, onSubscribeResolve, onSubscribeReject);
+    return promise_value;

Additional code (outside the diff range) to provide the reject handler:

// Near onSubscribeResolveImpl()
fn onSubscribeRejectImpl(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
    // thisArg is the JSValkeyClient; clean up subscription context on failure.
    const this_value = callframe.thisValue();
    const client = JSValkeyClient.fromJS(globalObject, this_value) orelse return callframe.argument(0); // forward error if not a client
    client.deleteSubscriptionCtx();
    return callframe.argument(0); // forward the rejection reason
}

// Export host-callable shims as needed by bun.cpp.JSC__JSValue__then
pub extern "C" fn onSubscribeResolve(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) jsc.JSValue {
    return (onSubscribeResolveImpl(globalObject, callframe) catch globalObject.takeAnyError()).asObjectRef();
}
pub extern "C" fn onSubscribeReject(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) jsc.JSValue {
    return (onSubscribeRejectImpl(globalObject, callframe) catch globalObject.takeAnyError()).asObjectRef();
}
#!/bin/bash
# Ensure the new handlers are referenced and there is no unused symbol left behind.
rg -n 'onSubscribeResolve|onSubscribeReject' src/valkey/js_valkey_functions.zig

Also applies to: 742-753

🧹 Nitpick comments (2)
src/valkey/valkey.zig (1)

175-183: Unify logging helper with Bun’s scoped logger pattern.

Use a consistent logger symbol and match the guideline default of disabled visibility. Also call the same symbol inside debug().

-    fn debug(this: *const ValkeyClient, comptime fmt: []const u8, args: anytype) void {
+    fn debug(this: *const ValkeyClient, comptime fmt: []const u8, args: anytype) void {
         if (this.cparent().identifier()) |ident| {
-            scopedDebug("({}) " ++ fmt, .{ident} ++ args);
+            log("({}) " ++ fmt, .{ident} ++ args);
         } else {
-            scopedDebug(fmt, args);
+            log(fmt, args);
         }
     }
-const scopedDebug = bun.Output.scoped(.Redis, .visible);
+const log = bun.Output.scoped(.Redis, false);

Also applies to: 1093-1093

src/valkey/js_valkey_functions.zig (1)

772-791: unsubscribe(): reconsider eager ctx deletion when unsubscribing from all.

Deleting the subscription context before UNSUBSCRIBE acks arrive may drop unsubscribe events and turn subsequent push acks into “response with no promise” logs. Consider deferring deleteSubscriptionCtx() until you’ve seen the final unsubscribe push with zero subscriptions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b53e0713e3b92c750f75c31e6d78783444309e2 and bea1e0567f59be4c305e97872e75d2ba57cf5b27.

📒 Files selected for processing (2)
  • src/valkey/js_valkey_functions.zig (20 hunks)
  • src/valkey/valkey.zig (16 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (2)
📚 Learning: 2025-08-30T00:13:36.784Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.784Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:07:54.560Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.560Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/valkey.zig
🔇 Additional comments (3)
src/valkey/valkey.zig (2)

439-478: Reconnect flow logging and guards look good.

Manual-close and auto-reconnect-off paths correctly short-circuit, and the backoff logging is helpful for debugging.


724-739: No action needed: subscriber-mode in-flight pairing is correctly handled. handleSubscribeResponse always resolves or rejects the promise before any early return, and the subsequent subscriber-only branches only run when no promise is dequeued, so there’s no risk of leaking an in-flight promise.

src/valkey/js_valkey_functions.zig (1)

44-46: LGTM on subscriber-mode guards.

Consistent “Cannot use in subscriber mode” checks prevent mixing regular commands with subscriber mode.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559, 850-856, 857-863, 864-870, 871-877, 878-884, 885-891, 892-898, 899-905, 906-912, 913-919, 920-926, 927-933, 934-940, 941-947, 948-954, 955-961, 962-968, 969-975, 976-982, 983-989, 990-996, 997-1003, 1004-1010, 1011-1017, 1018-1024, 1025-1031, 1032-1038, 1039-1045, 1046-1052, 1053-1059, 1060-1064, 1066-1071, 1072-1078, 1081-1086, 1087-1091, 1093-1097, 1098-1103, 1104-1109, 1110-1115, 1116-1121, 1122-1127, 1131-1136

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
@markovejnovic

markovejnovic commented Sep 3, 2025 •

Copy link
Copy Markdown
Contributor Author

Had a couple conversations at the office and we've decided to slightly rework the API. The API could be better. We want to achieve two things:

1. We want to change the interface to be EventEmitter-like.
2. We want to support --watch hot-reloading for code which uses Redis.

We hope to see this feature released with 1.3.

EDIT: Drop EventEmitter

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
src/valkey/valkey.zig (1)

621-665: Possible critical issue with subscriber response handling

The handleSubscribeResponse function fails the entire client connection on error responses from SUBSCRIBE/UNSUBSCRIBE commands. This is too aggressive - a failed subscription shouldn't tear down the client.

Apply this diff to handle errors gracefully:

-        switch (value.*) {
-            .Error => |err| {
-                promise_ptr.reject(
-                    globalThis,
-                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
-                );
-                this.fail(err, protocol.RedisError.InvalidResponse);
-            },
+        switch (value.*) {
+            .Error => |err| {
+                promise_ptr.reject(
+                    globalThis,
+                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
+                );
+                this.debug("subscribe command error: {s}", .{err});
+                return;
+            },
test/js/valkey/valkey.test.ts (2)

215-219: Use test harness URL instead of hardcoded localhost

Hardcoding the Redis URL will fail in CI environments where Redis may run on different ports.

 const connectedRedis = async () => {
-  const redis = new RedisClient(DEFAULT_REDIS_URL);
+  const redis = new RedisClient(DEFAULT_REDIS_URL);
   await redis.connect();
   return redis;
 };

Additionally, ensure proper cleanup in all tests using connectedRedis() to prevent connection leaks.


264-271: Replace arbitrary sleep with condition-based waiting

Using forEach(async ...) doesn't await the promises, and sleep(flushTimeoutMs) creates flaky tests. Apply the learning from previous reviews.

Based on the previous learning, implement deterministic waiting:

-Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-  expect(await redis.publish(testChannel, testMessage)).toBe(1);
-});
-
-// Wait a little bit just to ensure all the messages are flushed.
-await sleep(flushTimeoutMs);
+const done = Promise.withResolvers<void>();
+let currentCount = 0;
+
+await subscriber.subscribe(testChannel, (message, channel) => {
+  receiveCount++;
+  expect(channel).toBe(testChannel);
+  expect(message).toBe(testMessage);
+  if (++currentCount === TEST_MESSAGE_COUNT) done.resolve();
+});
+
+// Send messages sequentially
+for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+  expect(await redis.publish(testChannel, testMessage)).toBe(1);
+}
+
+// Wait for actual delivery with timeout
+await Promise.race([
+  done.promise,
+  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 3000))
+]);

This pattern should also be applied to tests at lines 294-295, 327-328, 363-364, 373-374, 405-406, 425-426, 452-453, 491-492, and 558-559.

src/valkey/js_valkey_functions.zig (3)

653-694: Fix error message label in publish function

The error message incorrectly references "set" instead of "publish".

 const message = (try fromJS(globalObject, callframe.argument(1))) orelse {
-    return globalObject.throwInvalidArgumentType("set", "message", "string or buffer or number");
+    return globalObject.throwInvalidArgumentType("publish", "message", "string or buffer or number");
 };

696-761: Subscribe function needs async error handling

The subscribe function only cleans up on immediate errors but not when the promise rejects later. Also fix the parameter name in the error message.

The subscription context cleanup should happen on async failures too. Consider attaching a rejection handler to the promise to ensure deleteSubscriptionCtx() is called on async failures.

Also fix:

 if (!handler_callback.isCallable()) {
-    return globalObject.throwInvalidArgumentType("subscribe", "message", "callable");
+    return globalObject.throwInvalidArgumentType("subscribe", "listener", "function");
 }

1137-1139: Pattern pub/sub exports without implementation

The psubscribe and punsubscribe functions are exported but there's no corresponding pmessage handler implementation. This will confuse users.

Either remove these exports or implement the full pattern subscription feature:

-pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
-pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
+// TODO: Pattern subscriptions not yet implemented
+// pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
+// pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
🧹 Nitpick comments (1)
test/js/valkey/valkey.test.ts (1)

553-563: Remove console.log statements from tests

Test output should be clean. Use proper test assertions instead of console logging.

-console.log("Publishing messages to channel with error callback");
 expect(await redis.publish(channel, "message1")).toBe(1);
 expect(await redis.publish(channel, "message2")).toBe(1);
 expect(await redis.publish(channel, "message3")).toBe(1);

 await sleep(flushTimeoutMs);

 expect(messageCount).toBe(3);

-console.log("Unsubscribing from channel after error test");
 await subscriber.unsubscribe(channel);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bea1e0567f59be4c305e97872e75d2ba57cf5b27 and fd0418d31e8675fa8d7e45b4654f79bfdfb62e9f.

📒 Files selected for processing (4)
  • packages/bun-types/redis.d.ts (1 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (15 hunks)
  • test/js/valkey/valkey.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bun-types/redis.d.ts
🧰 Additional context used
📓 Path-based instructions (11)
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Place tests in test/ and ensure filenames end with .test.ts or .test.tsx
Always use port: 0 in tests; never hardcode port numbers or use custom random-port functions
Prefer snapshot tests using normalizeBunSnapshot(...).toMatchInlineSnapshot(...) over exact string comparisons
Never write tests that assert absence of crashes (e.g., no 'panic' or 'uncaught exception' in output)
Avoid shell commands like find/grep in tests; use Bun.Glob and built-in tools instead

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript with Prettier (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Use bun:test in files that end in *.test.ts
Prefer async/await over callbacks in tests
For single-callback flows in tests, use Promise.withResolvers()
Do not set timeouts on tests; rely on Bun’s built-in timeouts
Use tempDirWithFiles from harness to create temporary directories/files in tests
Organize tests with describe blocks
Always assert exit codes and error scenarios in tests (e.g., proc.exited, expect(...).not.toBe(0), toThrow())
Use describe.each for parameterized tests; toMatchSnapshot for snapshots; and beforeAll/afterEach/beforeEach for setup/teardown
Avoid flaky tests: never wait for arbitrary time; wait for conditions to be met

Files:

  • test/js/valkey/valkey.test.ts
test/{**/*.test.ts,**/*-fixture.ts}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/{**/*.test.ts,**/*-fixture.ts}: When spawning Bun processes, use bunExe() and bunEnv from harness
Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup
Never hardcode port numbers; use port: 0 to get a random port
Use Buffer.alloc(count, fill).toString() instead of "A".repeat(count) for large/repetitive strings
Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Files:

  • test/js/valkey/valkey.test.ts
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (14)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/**/*.test.ts : Prefer async/await over callbacks in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/**/*.test.ts : Avoid flaky tests: never wait for arbitrary time; wait for conditions to be met

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : When spawning Bun processes, use bunExe() and bunEnv from harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:05:38.001Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-30T00:05:38.001Z
Learning: Applies to test/js/bun/**/*.test.{ts,tsx} : Put Bun-specific API tests under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:07:54.573Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/valkey.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
test/js/valkey/test-utils.ts (2)
  • DEFAULT_REDIS_URL (113-113)
  • randomCoinFlip (787-789)
packages/bun-types/redis.d.ts (2)
  • redis (750-750)
  • RedisClient (57-739)
🔇 Additional comments (8)
src/valkey/valkey.zig (4)

175-182: LGTM! Excellent addition of client-specific debug logging

The new debug method provides clear client identification in logs, making it much easier to debug multi-client scenarios.


1054-1056: Sensible const/non-const accessor pattern

The addition of cparent() alongside parent() provides a clean way to access the parent from const contexts. This is a reasonable pattern given the language constraints.


739-788: Good subscriber mode handling with clear branching hints

The subscriber mode message handling is well-structured with appropriate branch hints for optimization. The logic correctly handles both promise-based subscribe responses and server-initiated push messages.


315-329: Robust exponential backoff implementation

The reconnect delay calculation properly guards against integer overflow by capping attempts and using safe multiplication. Good defensive programming.

test/js/valkey/valkey.test.ts (2)

232-241: Good test coverage for subscriber mode restrictions

The test correctly validates that regular commands are blocked in subscriber mode with appropriate error messages.


276-300: Excellent message ordering verification test

Using UUIDs to verify message order is a smart approach that ensures uniqueness and proper sequencing.

src/valkey/js_valkey_functions.zig (2)

858-1135: Comprehensive subscriber mode guards added

Excellent work adding subscriber mode checks to all non-subscription commands. This prevents protocol violations and provides clear error messages.


763-855: Well-structured unsubscribe implementation

The unsubscribe function properly handles both full unsubscription (no args) and selective channel unsubscription, correctly managing the subscription context lifecycle.

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/valkey/js_valkey.zig (2)

735-744: Fix unbalanced ref/deref in onValkeyClose.

There’s a top-level defer this.deref() without a matching ref(). Keep the inner balanced pair around JS calls and remove the outer defer.

-        this.poll_ref.disable();
-        defer this.deref();
+        this.poll_ref.disable();

824-869: Connect: remove unbalanced ref() to prevent leaks.

this.ref() in connect() isn’t balanced; uWS and poll_ref keep the instance alive during async connect.

-        this.ref();
src/valkey/js_valkey_functions.zig (1)

219-246: expire(): potential use-after-scope — seconds_slice points to stack memory.
You pass a stack-backed slice via .raw; if send() reads async, this is UB. Use JSArgument like other commands.

Apply:

-    defer key.deinit();
@@
-    const seconds = try globalObject.validateIntegerRange(callframe.argument(1), i32, 0, .{
+    const seconds = try globalObject.validateIntegerRange(callframe.argument(1), i32, 0, .{
         .min = 0,
         .max = 2147483647,
         .field_name = "seconds",
     });
-
-    // Convert seconds to a string
-    var int_buf: [64]u8 = undefined;
-    const seconds_len = std.fmt.formatIntBuf(&int_buf, seconds, 10, .lower, .{});
-    const seconds_slice = int_buf[0..seconds_len];
-
-    // Send EXPIRE command
-    const promise = this.send(
+    _ = seconds; // validated, but we rely on fromJS for the payload
+
+    var args = try std.ArrayList(JSArgument).initCapacity(bun.default_allocator, 2);
+    defer {
+        for (args.items) |*item| item.deinit();
+        args.deinit();
+    }
+    try args.append(key); // transfer ownership to args
+    const seconds_arg = (try fromJS(globalObject, callframe.argument(1))) orelse unreachable;
+    try args.append(seconds_arg);
+
+    // Send EXPIRE command
+    const promise = this.send(
         globalObject,
         callframe.this(),
-        &.{
-            .command = "EXPIRE",
-            .args = .{ .raw = &.{ key.slice(), seconds_slice } },
-        },
+        &.{ .command = "EXPIRE", .args = .{ .args = args.items } },
     ) catch |err| {
         return protocol.valkeyErrorToJS(globalObject, "Failed to send EXPIRE command", err);
     };
♻️ Duplicate comments (3)
src/valkey/valkey.zig (1)

621-666: Don’t tear down the client on SUBSCRIBE/UNSUBSCRIBE errors. Reject the promise and continue.

Failing the whole client on a subscription command error is too aggressive and can drop unrelated work.

-            .Error => |err| {
-                promise_ptr.reject(
-                    globalThis,
-                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
-                );
-                this.fail(err, protocol.RedisError.InvalidResponse);
-            },
+            .Error => |_| {
+                promise_ptr.reject(
+                    globalThis,
+                    value.toJS(globalThis) catch |inner| globalThis.takeError(inner),
+                );
+                this.debug("subscribe command error", .{});
+                return;
+            },

Also consider using the existing Command alias consistently:

-fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *ValkeyCommand.PromisePair) ?void {
+fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *Command.PromisePair) ?void {
src/valkey/js_valkey.zig (1)

806-811: Finalize: remove duplicate deref (double-decrement).

You already defer a deref at the start of finalize.

-        if (this.subscription_ctx) |*subscription_ctx| {
+        if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();
src/valkey/js_valkey_functions.zig (1)

1184-1190: Pattern pub/sub surface: ensure pmessage is fully wired or gate exports.
If pattern message dispatch isn’t implemented end-to-end, hide psubscribe/punsubscribe for now to avoid misleading users.

Run to verify handler coverage:

#!/bin/bash
rg -n '\bpmessage\b|\bpsubscribe\b|\bpunsubscribe\b' src/valkey -C2

Also applies to: 809-815

🧹 Nitpick comments (9)
packages/bun-types/redis.d.ts (2)

659-666: Fix typos and SEE references in JSDoc.

  • “pipeling” → “pipelining”
  • “@see @subscribe” → “@see subscribe”
  • “sunsunscribe” → “sunsubscribe”
  • Use “@PARAM” (not “@parm”).

Apply:

- * @note Subscribing disables automatic pipeling, so all commands will be
+ * @note Subscribing disables automatic pipelining, so all commands will be
@@
- *       unsubscribed. Only @see ping, @see psubscribe, @see punsubscribe,
- *       @see ssubscribe, @see @subscribe, @see sunsunscribe and @see
+ *       unsubscribed. Only @see ping, @see psubscribe, @see punsubscribe,
+ *       @see ssubscribe, @see subscribe, @see sunsubscribe and @see
@@
- * @note Subscribing disables automatic pipeling, so all commands will be
+ * @note Subscribing disables automatic pipelining, so all commands will be
@@
- * @note Unsubscribing moves the channel back to a normal state out of the
+ * @note Unsubscribing moves the channel back to a normal state, out of the
@@
- * @parm channels An array of channels to unsubscribe from.
+ * @param channels An array of channels to unsubscribe from.

Also applies to: 678-683, 703-706, 712-712


646-667: Clarify subscribe() return value in docs.

The type returns Promise. Add a @returns line noting Redis returns the number of channels currently subscribed after the operation.

Also applies to: 669-684

src/valkey/valkey.zig (1)

1092-1092: Gate logs: make scoped logger non-visible by default.

Follow the guideline to avoid noisy logs in production and use the debug helpers to opt-in.

-const scopedDebug = bun.Output.scoped(.Redis, .visible);
+const scopedDebug = bun.Output.scoped(.Redis, false);

If needed, add a runtime switch (e.g., Options.enable_debug_logging) to opt-in per client.

src/valkey/js_valkey.zig (2)

1027-1078: Use this.debug for handshake logs for consistent client identifiers.

Minor: prefer this.debug(...) over scopedDebug(...) to include per-client id.

-            scopedDebug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });
+            this.debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });

1154-1154: Gate JS-side logs: make scoped logger non-visible by default.

Align with logging guidance.

-const scopedDebug = bun.Output.scoped(.RedisJS, .visible);
+const scopedDebug = bun.Output.scoped(.RedisJS, false);
src/valkey/js_valkey_functions.zig (4)

449-471: Use pointer iteration when deinit-ing Slices.
Be explicit to avoid deinit-ing a copy.

Apply:

-    defer {
-        for (args.items) |item| {
-            item.deinit();
-        }
-        args.deinit();
-    }
+    defer {
+        for (args.items) |*item| item.deinit();
+        args.deinit();
+    }

Repeat in both hmget() and hmset().

Also applies to: 575-581


1196-1200: Typo in comment.
“getrang” → “getrange”.

Apply:

-// substr(key: RedisKey, start: number, end: number)` // Deprecated alias for getrang
+// substr(key: RedisKey, start: number, end: number)` // Deprecated alias for getrange

621-651: Optional: add scoped debug logs for subscribe/unsubscribe/ping.
Tiny logs around entry/exit help trace hot-reload and event plumbing.

Example:

+const log = bun.Output.scoped(.valkey_pubsub, false);
@@
-    const promise = this.send(
+    log("SUBSCRIBE {} channels", .{redis_channels.items.len});
+    const promise = this.send(

Also applies to: 696-807, 1097-1190


96-124: Add “number” to error messages for fromJS coercions
fromJS() accepts numbers but the throwInvalidArgumentType calls in src/valkey/js_valkey_functions.zig (e.g. sendCommand, get, set, srem, ping, etc.) only list “string or buffer.” Update those error messages—and the corresponding d.ts definitions—to include “number.”

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fd0418d31e8675fa8d7e45b4654f79bfdfb62e9f and 5c3882099136159c401aeb3db75a3ee16f49c996.

📒 Files selected for processing (4)
  • packages/bun-types/redis.d.ts (1 hunks)
  • src/valkey/js_valkey.zig (13 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (15 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript with Prettier (bun run prettier)

Files:

  • packages/bun-types/redis.d.ts
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (7)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • packages/bun-types/redis.d.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:07:54.573Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🔇 Additional comments (3)
packages/bun-types/redis.d.ts (1)

729-738: Note argument order difference vs on("message").

subscribe listeners are (message, channel) while on("message") is (channel, message). Call this out explicitly to avoid confusion.

Would you like me to add a short note in these blocks calling out the intentional difference?

src/valkey/js_valkey.zig (1)

656-672: Ensure subscription mode exits when last channel unsubscribes.

onValkeyUnsubscribe currently ignores the payload; when total subscription count reaches 0, restore flags and delete the SubscriptionCtx.

Would you like me to wire this here by parsing the unsubscribe push (count==0) and calling deleteSubscriptionCtx(), or is this already handled in js_valkey_functions.zig?

Also applies to: 356-366

src/valkey/js_valkey_functions.zig (1)

44-46: LGTM: consistent subscriber-mode guards across commands.
Good call preventing non-pubsub ops while subscribed.

Also applies to: 68-70, 92-94, 141-145, 165-169, 189-193, 214-218, 249-253, 275-277, 304-307, 329-331, 354-357, 379-382, 408-411, 438-441, 489-492, 523-526, 557-560, 819-823, 825-831, 833-838, 840-845, 847-852, 854-859, 861-865, 867-872, 874-879, 882-887, 889-894, 896-901, 903-907, 910-915, 917-922, 924-929, 931-936, 938-943, 945-950, 952-957, 959-964, 966-971, 973-978, 980-985, 986-991, 992-997, 998-1003, 1004-1008, 1010-1015, 1016-1021, 1022-1026, 1028-1032, 1035-1039, 1041-1045, 1049-1053, 1055-1059, 1061-1065, 1067-1071, 1073-1077, 1079-1083, 1085-1089, 1091-1095

Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/bun-types/redis.d.ts (1)

654-657: Fix example arg order to match the signature (message, channel).

The callback currently shows (channel, message).

- * await client.subscribe("my-channel", (channel, message) => {
+ * await client.subscribe("my-channel", (message, channel) => {
  *   console.log(`Received message on ${channel}: ${message}`);
  * });
🧹 Nitpick comments (4)
packages/bun-types/redis.d.ts (4)

640-644: Clarify publish() return semantics; drop uncertain cluster-node claim.

Keep docs accurate and implementation-agnostic.

-   * @returns The number of clients that received the message. Note that in a
-   *          cluster this returns the total number of clients in the same
-   *          node.
+   * @returns The number of subscribers that received the message.

686-696: Tighten language: “single” (not “singular”); state is connection-level, not per-channel.

- * Unsubscribe from a singular Redis channel.
+ * Unsubscribe from a single Redis channel.
@@
- * @note If there are no more channels subscribed to, the client
- *       automatically re-enables pipelining if it was previously enabled.
- * @note Unsubscribing moves the channel back to a normal state out of the
- *       subscription state if all channels have been unsubscribed from.
- *       For further details on the subscription state, @see subscribe.
+ * @note If no channels remain after this call, the connection exits Pub/Sub
+ *       mode and automatic pipelining (if previously enabled) is re-enabled.
+ *       See subscribe() for details about Pub/Sub mode.

701-707: Clarify unsubscribe() behavior at connection level.

- * @note The client will automatically re-enable pipelining if it was
- *       previously enabled.
- * @note Unsubscribing moves the channel back to a normal state out of the
- *       subscription state if all channels have been unsubscribed from.
- *       For further details on the subscription state, @see subscribe.
+ * @note Unsubscribing from all channels exits Pub/Sub mode. Automatic
+ *       pipelining (if previously enabled) is re-enabled. See subscribe()
+ *       for details about Pub/Sub mode.

722-729: Call out parameter-order difference vs subscribe() for on("message").

Prevents confusion since subscribe uses (message, channel) but on("message") uses (channel, message).

  /**
   * Handle messages coming from the Redis server.
   *
   * @note This interface, contrary to `ioredis`, is not the recommended way
   *       to handle messages. We suggest using the `subscribe` method's
   *       listener instead.
+  * @note Listener signature here is (channel, message). This intentionally
+  *       differs from subscribe(), which uses (message, channel).
   */
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c3882099136159c401aeb3db75a3ee16f49c996 and dae930ed0f5cbe06a37fbeec3e86f15781dc546f.

📒 Files selected for processing (2)
  • packages/bun-types/redis.d.ts (1 hunks)
  • src/valkey/valkey.zig (15 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/valkey/valkey.zig
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript with Prettier (bun run prettier)

Files:

  • packages/bun-types/redis.d.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • packages/bun-types/redis.d.ts
🔇 Additional comments (2)
packages/bun-types/redis.d.ts (2)

731-738: LGTM: error event handler typing looks good.


659-666: Fix Pub/Sub docs: typo and allowed commands list

- * @note Subscribing disables automatic pipeling, so all commands will be
- *       received immediately.
+ * @note Subscribing disables automatic pipelining and switches the connection
+ *       into Pub/Sub mode.
- * @note Subscribing moves the channel to a dedicated subscription state
- *       which prevents most other commands from being executed until
- *       unsubscribed. Only @see ping, @see psubscribe, @see punsubscribe,
- *       @see ssubscribe, @see @subscribe, @see sunsunscribe and @see
- *       unsubscribe are legal to invoke in a subscribed upon channel.
+ * @note While subscribed, only PING and subscription‐management commands are
+ *       allowed: SUBSCRIBE/UNSUBSCRIBE, PSUBSCRIBE/PUNSUBSCRIBE, and
+ *       SSUBSCRIBE/SUNSUBSCRIBE.
+ *       Ensure these methods exist on RedisClient or adjust the typings accordingly.

Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread packages/bun-types/redis.d.ts Outdated

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

Actionable comments posted: 3

♻️ Duplicate comments (15)
packages/bun-types/redis.d.ts (3)

678-679: Fix typo in multi-channel subscribe docs.

Same typo appears here.

Apply this diff:

-     * @note Subscribing disables automatic pipeling, so all commands will be
+     * @note Subscribing disables automatic pipelining, so all commands will be

712-712: Fix typo: "@parm" → "@PARAM".

JSDoc parameter tag is misspelled.

Apply this diff:

-     * @parm channels An array of channels to unsubscribe from.
+     * @param channels An array of channels to unsubscribe from.

659-660: Fix typo: "pipeling" → "pipelining".

The documentation contains a typo that should be corrected.

Apply this diff to fix the typo:

-     * @note Subscribing disables automatic pipeling, so all commands will be
+     * @note Subscribing disables automatic pipelining, so all commands will be
test/js/valkey/valkey.test.ts (3)

215-219: Use test harness URL instead of hardcoded localhost.

Hardcoding redis://localhost:6379 will fail on CI. Use DEFAULT_REDIS_URL from test harness and ensure connections are cleaned up.

Apply this diff:

     const connectedRedis = async () => {
-      const redis = new RedisClient("redis://localhost:6379");
+      const redis = new RedisClient(DEFAULT_REDIS_URL);
       await redis.connect();
       return redis;
     };

Additionally, ensure all tests that use connectedRedis() properly close the clients to avoid connection leaks. Add cleanup in tests:

// Example cleanup pattern
const redis = await connectedRedis();
const subscriber = await connectedRedis();
try {
  // ... test logic ...
} finally {
  redis.close();
  subscriber.close();
}

264-271: Avoid forEach(async) and arbitrary sleeps; wait for subscriber conditions.

Using forEach(async ...) doesn't await the promises, and sleep(flushTimeoutMs) is flaky. Instead, wait for the subscriber to receive all messages with a timeout.

Based on the learning, here's the improved pattern:

-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      const done = Promise.withResolvers<void>();
+      
+      var receiveCount = 0;
+      await subscriber.subscribe(testChannel, (message, channel) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) {
+          done.resolve();
+        }
+      });
+
+      // Send all messages sequentially
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+
+      // Wait for subscriber to receive all messages with timeout
+      await Promise.race([
+        done.promise,
+        new Promise((_, reject) => 
+          setTimeout(() => reject(new Error("Timeout waiting for messages")), 3000)
+        )
+      ]);

This pattern should also be applied to the other tests that use similar forEach(async) + sleep() patterns at lines: 287-295, 327-328, 363-364, 373-374, 405-406, 425-426, 452-453, 491-492, 558-559.


221-565: Add cleanup for Redis connections in all PUB/SUB tests.

Many tests don't properly close their Redis connections, which could lead to connection leaks on CI.

Each PUB/SUB test should follow this pattern to ensure proper cleanup:

test("example test", async () => {
  const redis = await connectedRedis();
  const subscriber = await connectedRedis();
  
  try {
    // test logic here
  } finally {
    redis.close();
    subscriber.close(); 
  }
});

Alternatively, use an afterEach hook in the describe block:

   describe("PUB/SUB", () => {
+    let openConnections: RedisClient[] = [];
+
+    const trackedConnectedRedis = async () => {
+      const redis = new RedisClient(DEFAULT_REDIS_URL);
+      await redis.connect();
+      openConnections.push(redis);
+      return redis;
+    };
+
+    afterEach(() => {
+      for (const conn of openConnections) {
+        conn.close();
+      }
+      openConnections = [];
+    });

     const testChannel = "test-channel";
     // ... rest of test setup ...

-    const connectedRedis = async () => {
-      const redis = new RedisClient(DEFAULT_REDIS_URL);
-      await redis.connect();
-      return redis;
-    };
+    const connectedRedis = trackedConnectedRedis;
src/valkey/valkey.zig (2)

392-392: Use @tagName for enum, not @errorName.

protocol.RedisError is an enum type, not an error union, so @errorName is incorrect.

Apply this diff:

-        this.debug("failed: {s}: {}", .{ message, err });
+        this.debug("failed: {s}: {s}", .{ message, @tagName(err) });

736-796: Defer loading promise pair until needed to avoid dropping promises.

The current implementation unconditionally reads and removes a promise from the queue before checking if the value actually requires it. This can drop promises permanently for unrelated Push messages.

Restructure to only read the promise when needed:

-        // Let's load the promise pair.
-        var pair_maybe = this.in_flight.readItem();
+        // Defer loading the promise pair until we know we need it
+        var pair_maybe: ?ValkeyCommand.PromisePair = null;

         // We handle subscriptions specially because they are not regular
         // commands and their failure will potentially cause the client to drop
         // out of subscriber mode.
         if (this.cparent().isSubscriber()) {
             this.debug("This client is a subscriber. Handling as subscriber...", .{});

-            // There are multiple different commands we may receive in
-            // subscriber mode. One is from a client.subscribe() call which
-            // requires that a promise is in-flight, but otherwise, we may also
-            // receive push messages from the server that do not have an
-            // associated promise.
-            if (pair_maybe) |*pair| {
-                this.debug("There is a request in flight. Handling as a subscribe request...", .{});
-                const res = this.handleSubscribeResponse(value, pair);
-                if (res != null) {
-                    return;
-                }
-            }
-
             switch (value.*) {
                 .Error => |err| {
                     this.fail(err, protocol.RedisError.InvalidResponse);
                     return;
                 },
                 .Push => |push| {
                     if (std.mem.eql(u8, push.kind, "message")) {
                         @branchHint(.likely);
                         this.debug("Received a message.", .{});
                         this.onValkeyMessage(push.data);
                         return;
                     } else if (std.mem.eql(u8, push.kind, "subscribe")) {
-                        @branchHint(.cold);
-                        this.debug("Received subscription message without promise: {any}", .{push.data});
+                        // Now read the promise pair and resolve it
+                        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+                        if (pair_maybe) |*pair| {
+                            this.debug("Handling subscribe response...", .{});
+                            _ = this.handleSubscribeResponse(value, pair) orelse {};
+                        } else {
+                            @branchHint(.cold);
+                            this.debug("Received subscription message without promise: {any}", .{push.data});
+                        }
                         return;
                     } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                        @branchHint(.cold);
-                        this.debug("Received unsubscribe message without promise: {any}", .{push.data});
+                        // Now read the promise pair and resolve it  
+                        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+                        if (pair_maybe) |*pair| {
+                            this.debug("Handling unsubscribe response...", .{});
+                            _ = this.handleSubscribeResponse(value, pair) orelse {};
+                        } else {
+                            @branchHint(.cold);
+                            this.debug("Received unsubscribe message without promise: {any}", .{push.data});
+                        }
                         return;
                     } else {
                         @branchHint(.cold);
                         this.fail("Unexpected push message kind without promise", protocol.RedisError.InvalidResponseType);
                         return;
                     }
                 },
                 else => {
+                    // For non-push responses in subscriber mode, we may need a promise
+                    if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+                    if (pair_maybe) |*pair| {
+                        this.debug("There is a request in flight. Handling as a subscribe request...", .{});
+                        const res = this.handleSubscribeResponse(value, pair);
+                        if (res != null) {
+                            return;
+                        }
+                    }
                     // In the else case, we fall through to the regular
                     // handler. Subscribers can send .Push commands which have
                     // the same semantics as regular commands.
                 },
             }

             this.debug("Treating subscriber response as a regular command...", .{});
         }

         // For regular commands, get the next command+promise pair from the queue
+        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
         var pair = pair_maybe orelse {
             this.debug("Received response but no promise in queue", .{});
             return;
         };
src/valkey/js_valkey.zig (3)

92-101: Treat null like undefined when initializing handler arrays.

-            if (value.isUndefined()) {
-                // Create a new array if the value is undefined/null
+            if (value.isUndefinedOrNull()) {
+                // Create a new array if the value is undefined or null
                 handlers_array = try jsc.JSArray.createEmpty(globalObject, 1);

722-731: Don’t close the client if a subscriber callback throws.

Catching and calling failWithJSValue tears down the connection on user-land errors.

-        ) catch |e| {
-            this.debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
-        };
+        ) catch |e| {
+            this.debug("Subscriber callback threw: {}", .{e});
+            globalObject.reportActiveExceptionAsUnhandled(e);
+        };

821-826: Double deref in finalize — remove the explicit call.

         if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();

The earlier defer already decrefs.

src/valkey/js_valkey_functions.zig (4)

788-807: unsubscribe(all): defer context deletion until Promise resolves.

-        const promise = this.send(
+        const promise = this.send(
             globalObject,
             callframe.this(),
             &command,
         ) catch |err| {
             return protocol.valkeyErrorToJS(globalObject, "Failed to send UNSUBSCRIBE command", err);
         };
-
-        // Clear all subscriptions and exit subscription mode
-        this.deleteSubscriptionCtx();
-
-        return promise.toJS();
+        const promise_value = promise.toJS();
+        // Exit subscription mode after UNSUBSCRIBE-all resolves.
+        bun.cpp.JSC__JSValue__then(promise_value, globalObject, this.onUnsubscribeAllResolve(), jsc.JSValue.zero());
+        return promise_value;

701-769: subscribe(): cleanup on async rejection is missing. Attach a rejection handler.

     const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         // If we find an error, we need to clean up the subscription context.
         this.deleteSubscriptionCtx();
         return protocol.valkeyErrorToJS(globalObject, "Failed to send SUBSCRIBE command", err);
     };
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    // Ensure cleanup if the subscribe Promise later rejects.
+    bun.cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

If onSubscribeReject() is not present, I can provide a minimal host-callback that calls deleteSubscriptionCtx() and rethrows.


849-870: unsubscribe(channels): only exit subscriber mode after UNSUBSCRIBE resolves.

-    const promise = this.send(
+    const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         return protocol.valkeyErrorToJS(globalObject, "Failed to send UNSUBSCRIBE command", err);
     };
-
-    // Check if we have any remaining subscriptions
-    // If the callback map is empty, we can exit subscription mode
-    if (!subscription_ctx.hasSubscriptions(globalObject)) {
-        // No more subscriptions, exit subscription mode
-        this.deleteSubscriptionCtx();
-    }
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    if (!subscription_ctx.hasSubscriptions(globalObject)) {
+        // Schedule exit once the server acknowledges.
+        bun.cpp.JSC__JSValue__then(promise_value, globalObject, this.onUnsubscribeAllResolve(), jsc.JSValue.zero());
+    }
+    return promise_value;

653-699: Add subscriber-mode guard to publish() and fix error text.

Publishing while subscribed is disallowed; also the error still mentions buffer/number despite isString().

 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (5)
src/valkey/valkey.zig (1)

1057-1059: Consider extracting const/non-const pattern to a helper.

The inline functions for const/non-const parent access work well. If this pattern is used elsewhere in the codebase, consider extracting it to a common utility.

This pattern is clean for now. If you need this elsewhere, you could create a generic helper macro/function for field parent access with const-correctness.

src/valkey/js_valkey.zig (3)

15-19: Keep protect/unprotect; drop the TODO to avoid accidental removal.

The map must stay GC-rooted while holding callbacks. Remove the “Delete this .protect()” TODO; deinit already unprotects.

-        // TODO(markovejnovic): Delete this .protect()
         self._callback_map.protect();

Also applies to: 158-161


1043-1044: Use this.debug for consistency and client identifier tagging.

-            scopedDebug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });
+            this.debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });

1169-1169: Optional: align scoped logger with house style (non-visible by default).

-const scopedDebug = bun.Output.scoped(.RedisJS, .visible);
+const scopedDebug = bun.Output.scoped(.RedisJS, false);

Your this.debug() helper already controls verbosity well.

src/valkey/js_valkey_functions.zig (1)

5-12: Minor: simplify jsSend array check.

-    if (!args_array.isObject() or !args_array.isArray()) {
+    if (!args_array.isArray()) {
         return globalObject.throw("Arguments must be an array", .{});
     }

The object check is redundant.

Also applies to: 18-22

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dae930ed0f5cbe06a37fbeec3e86f15781dc546f and 590826ece99eb3f207bb4eb1e72813938933fecc.

📒 Files selected for processing (5)
  • packages/bun-types/redis.d.ts (1 hunks)
  • src/valkey/js_valkey.zig (13 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (15 hunks)
  • test/js/valkey/valkey.test.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript with Prettier (bun run prettier)

Files:

  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Place tests in test/ and ensure filenames end with .test.ts or .test.tsx
Always use port: 0 in tests; never hardcode port numbers or use custom random-port functions
Prefer snapshot tests using normalizeBunSnapshot(...).toMatchInlineSnapshot(...) over exact string comparisons
Never write tests that assert absence of crashes (e.g., no 'panic' or 'uncaught exception' in output)
Avoid shell commands like find/grep in tests; use Bun.Glob and built-in tools instead

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Use bun:test in files that end in *.test.ts
Prefer async/await over callbacks in tests
For single-callback flows in tests, use Promise.withResolvers()
Do not set timeouts on tests; rely on Bun’s built-in timeouts
Use tempDirWithFiles from harness to create temporary directories/files in tests
Organize tests with describe blocks
Always assert exit codes and error scenarios in tests (e.g., proc.exited, expect(...).not.toBe(0), toThrow())
Use describe.each for parameterized tests; toMatchSnapshot for snapshots; and beforeAll/afterEach/beforeEach for setup/teardown
Avoid flaky tests: never wait for arbitrary time; wait for conditions to be met

Files:

  • test/js/valkey/valkey.test.ts
test/{**/*.test.ts,**/*-fixture.ts}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/{**/*.test.ts,**/*-fixture.ts}: When spawning Bun processes, use bunExe() and bunEnv from harness
Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup
Never hardcode port numbers; use port: 0 to get a random port
Use Buffer.alloc(count, fill).toString() instead of "A".repeat(count) for large/repetitive strings
Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Files:

  • test/js/valkey/valkey.test.ts
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
🧠 Learnings (18)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
  • src/valkey/js_valkey_functions.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/**/*.test.ts : Prefer async/await over callbacks in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/**/*.test.ts : Avoid flaky tests: never wait for arbitrary time; wait for conditions to be met

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/**/*.test.ts : For single-callback flows in tests, use Promise.withResolvers()

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Use using or await using for Bun APIs (e.g., Bun.spawn, Bun.listen, Bun.connect, Bun.serve) to ensure cleanup

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : When spawning Bun processes, use bunExe() and bunEnv from harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:07:13.641Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-08-30T00:07:13.641Z
Learning: Applies to test/{**/*.test.ts,**/*-fixture.ts} : Import testing utilities from harness (bunExe, bunEnv, tempDirWithFiles, tmpdirSync, isMacOS, isWindows, isPosix, gcTick, withoutAggressiveGC)

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:05:38.001Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-30T00:05:38.001Z
Learning: Applies to test/js/bun/**/*.test.{ts,tsx} : Put Bun-specific API tests under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:07:54.573Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
test/js/valkey/test-utils.ts (2)
  • DEFAULT_REDIS_URL (113-113)
  • randomCoinFlip (787-789)
packages/bun-types/redis.d.ts (2)
  • redis (732-732)
  • RedisClient (57-721)
🔇 Additional comments (5)
packages/bun-types/redis.d.ts (1)

655-657: LGTM! Documentation example corrected.

The example now correctly shows the listener parameters in the right order (message, channel) matching the documented signature.

test/js/valkey/valkey.test.ts (2)

1-11: Approved: Imports correctly reference test utilities.

The imports are properly structured and include the necessary utilities from test-utils.

Based on the retrieved learning, I'll apply it to ensure tests properly account for Redis server→subscriber propagation.


407-410: Test expectation is correct - both listeners should receive messages.

The test correctly expects both listeners to be called. This is consistent with the current implementation where multiple subscriptions to the same channel are allowed and all listeners receive messages.

src/valkey/valkey.zig (1)

174-182: Good addition of per-client debugging.

The debug helper with client identifier prefix will make troubleshooting multi-client scenarios much easier.

src/valkey/js_valkey_functions.zig (1)

43-66: LGTM: subscriber-mode guards + thin wrappers look solid.

Good consistency on guarding non-PUB/SUB commands and delegating to existing compiled bindings.

Also applies to: 67-90, 91-139, 141-163, 165-187, 189-212, 214-247, 249-271, 273-300, 302-325, 327-350, 352-375, 377-404, 406-434, 436-485, 487-519, 521-553, 555-619, 880-907, 908-936, 937-955, 956-984, 985-998, 999-1005, 1006-1012, 1013-1019, 1020-1026, 1027-1039, 1040-1046, 1047-1057, 1058-1064, 1065-1071, 1072-1081, 1082-1088, 1089-1095, 1096-1101, 1104-1114, 1115-1121, 1122-1128, 1129-1135, 1136-1144, 1145-1150, 1168-1349

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/valkey/valkey.zig (1)

529-541: Pass protocol.RedisError enum to fail(); map parser errors instead of forwarding Zig errors.

fail() expects protocol.RedisError, but you pass a Zig error (err or error.InvalidResponse). This won’t compile and/or miscategorizes failures.

Add a mapper and use it:

@@
 pub const ValkeyClient = struct {
@@
+    inline fn mapParserErrorToRedisError(_: anytype) protocol.RedisError {
+        // Expand as needed
+        return protocol.RedisError.InvalidResponse;
+    }
@@
-                var value = reader.readValue(this.allocator) catch |err| {
+                var value = reader.readValue(this.allocator) catch |err| {
                     if (err == error.InvalidResponse) {
                         // Need more data in the buffer, wait for next onData call
@@
-                    } else {
-                        this.fail("Failed to read data (buffer path)", err);
+                    } else {
+                        this.fail("Failed to read data (buffer path)", mapParserErrorToRedisError(err));
                         return;
                     }
                 };
@@
-                if (bytes_consumed == 0 and remaining_buffer.len > 0) {
-                    this.fail("Parser consumed 0 bytes unexpectedly (buffer path)", error.InvalidResponse);
+                if (bytes_consumed == 0 and remaining_buffer.len > 0) {
+                    this.fail("Parser consumed 0 bytes unexpectedly (buffer path)", protocol.RedisError.InvalidResponse);
                     return;
                 }
@@
-            var value = reader.readValue(this.allocator) catch |err| {
+            var value = reader.readValue(this.allocator) catch |err| {
                 if (err == error.InvalidResponse) {
@@
-                } else {
+                } else {
                     // Any other error is fatal
-                    this.fail("Failed to read data (stack path)", err);
+                    this.fail("Failed to read data (stack path)", mapParserErrorToRedisError(err));
                     return;
                 }
             };
@@
-            if (bytes_consumed == 0) {
+            if (bytes_consumed == 0) {
                 // This case should ideally not happen if readValue succeeded and slice wasn't empty
-                this.fail("Parser consumed 0 bytes unexpectedly (stack path)", error.InvalidResponse);
+                this.fail("Parser consumed 0 bytes unexpectedly (stack path)", protocol.RedisError.InvalidResponse);
                 return;
             }

Also applies to: 543-547, 571-586, 590-595

♻️ Duplicate comments (9)
src/valkey/js_valkey.zig (5)

73-75: Compare JS callbacks by identity, not Zig “==”.

Use strict JS equality to avoid false negatives.

-            if (iter == callback) {
+            if (iter.strictEquals(globalObject, callback)) {
                 continue;
             }

100-103: Treat null like undefined when initializing handler arrays.

Prevents assert paths when value is null.

-            if (value.isUndefined()) {
+            if (value.isUndefinedOrNull()) {
                 // Create a new array if the value is undefined/null
                 handlers_array = try jsc.JSArray.createEmpty(globalObject, 1);

827-831: Finalize: remove extra deref (double-decrement).

There’s already a defer this.deref() at the top.

         if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();

733-737: Do not tear down the connection if a subscriber callback throws.

Convert to “report error, keep connection alive”.

-        ) catch |e| {
-            this.debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
-        };
+        ) catch |e| {
+            this.debug("Subscriber callback threw: {}", .{e});
+            globalObject.reportActiveExceptionAsUnhandled(e);
+        };

1027-1028: Missing symbol: fns.on is not defined in js_valkey_functions.zig.

Either implement on(...) there or drop this alias to avoid build break.

-    pub const on = fns.on;
+    // pub const on = fns.on; // Uncomment when implemented in js_valkey_functions.zig
src/valkey/js_valkey_functions.zig (4)

701-769: subscribe(): attach async rejection handler to clean up subscription state; reject empty array early.

Without a catch, a later rejection leaves the client stuck in subscriber mode.

     if (channelOrMany.isArray()) {
-        if ((try channelOrMany.getLength(globalObject)) == 0) {
+        if ((try channelOrMany.getLength(globalObject)) == 0) {
             return globalObject.toInvalidArguments("subscribe requires at least one channel", .{});
         }
@@
-    const promise = this.send(
+    const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         // If we find an error, we need to clean up the subscription context.
         this.deleteSubscriptionCtx();
         return protocol.valkeyErrorToJS(globalObject, "Failed to send SUBSCRIBE command", err);
     };
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    // Cleanup subscription ctx if the subscribe promise rejects later.
+    cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

(Ensure onSubscribeReject() calls deleteSubscriptionCtx() and rethrows.)


792-811: unsubscribe(all): defer ctx deletion until command resolves.

Deleting immediately can desync state if UNSUBSCRIBE later fails.

-        // Clear all subscriptions and exit subscription mode
-        this.deleteSubscriptionCtx();
-
-        return promise.toJS();
+        const promise_value = promise.toJS();
+        const cpp = bun.cpp;
+        cpp.JSC__JSValue__then(promise_value, globalObject, this.onUnsubscribeAllResolve(), jsc.JSValue.zero());
+        return promise_value;

1180-1182: Gate or remove PSUBSCRIBE/PUNSUBSCRIBE until “pmessage” is handled.

Exposing pattern APIs without dispatching pmessage will confuse users.

-pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
-pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
+// TODO: Implement pmessage dispatch end-to-end before exposing:
+// pub const psubscribe = ...
+// pub const punsubscribe = ...

If pattern messages are implemented elsewhere, point to the handler; otherwise keep hidden.


653-699: publish(): add subscriber-mode guard and fix error text for message type.

In subscriber mode, commands like PUBLISH should be disallowed. Also error text says “string or buffer or number” but you require a string.

 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (7)
src/valkey/valkey.zig (3)

1097-1098: Remove duplicate ValkeyCommand alias and use Command consistently.

Avoid two aliases for the same module.

-const ValkeyCommand = @import("./ValkeyCommand.zig");
+// (alias not needed; use Command)

And update the signature:

-fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *ValkeyCommand.PromisePair) ?void {
+fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *Command.PromisePair) ?void {

Also applies to: 621-621


736-743: Optional: add a debug hint before touching in_flight.

Helps trace rare “no promise in queue” paths.

-        // Let's load the promise pair.
-        var pair_maybe = this.in_flight.readItem();
+        // Defer loading the promise pair until it’s required (see below).
+        var pair_maybe: ?Command.PromisePair = null;

392-392: Enum logging: prefer @TagName(err) or {s} with @TagName to avoid braces noise.

Minor readability improvement.

-        this.debug("failed: {s}: {}", .{ message, err });
+        this.debug("failed: {s}: {s}", .{ message, @tagName(err) });
src/valkey/js_valkey.zig (1)

554-587: Nit: use specific enum for timeout in onConnectionTimeout.

Consider .ConnectionTimeout instead of .ConnectionClosed.

-                this.clientFail(msg, protocol.RedisError.ConnectionTimeout);
+                this.clientFail(msg, protocol.RedisError.ConnectionTimeout);

(If a .ConnectionTimeout tag already exists; otherwise ignore.)

src/valkey/js_valkey_functions.zig (3)

1-41: jsSend(): consider guarding subscriber mode or documenting that it bypasses the guard.

If you intend jsSend to be “escape hatch,” document; else add the same guard.

+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }

(If you want to restrict raw sends while subscribed.)


1378-1395: fromJS(): numeric-to-string conversion is fine; ensure large integers round-trip.

If BigInt inputs are expected, consider explicit BigInt handling.


701-769: Tests: avoid sleeps in PUB/SUB; await on subscriber callback with timeout.

Match the retrieved learning for reliability.

I can draft a test helper using Promise.withResolvers + withTimeout and resolve from the message callback to remove flakiness.

Also applies to: 771-898, 653-699

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 590826ece99eb3f207bb4eb1e72813938933fecc and 1c3ac3d169af6562acd530438bc1cf57a926c4b2.

📒 Files selected for processing (5)
  • packages/bun-types/redis.d.ts (2 hunks)
  • src/valkey/js_valkey.zig (13 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (15 hunks)
  • test/js/valkey/valkey.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zig: Format Zig files with zig fmt (bun run zig-format)
In Zig code, manage memory carefully: use the correct allocator and defer for cleanup

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
🧠 Learnings (7)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:07:54.573Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
🔇 Additional comments (2)
src/valkey/valkey.zig (1)

736-743: No additional premature in_flight.readItem() calls found.

src/valkey/js_valkey_functions.zig (1)

621-651: ping(): allowed in subscriber mode — OK.

No action needed; just acknowledging the special-case.

Comment thread src/valkey/valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated
@markovejnovic

Copy link
Copy Markdown
Contributor Author

Need to figure out what node-redis' behavior for registering the same listener multiple times is.

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/valkey/js_valkey.zig (1)

775-783: Unbalanced ref/deref in onValkeyClose.

There’s a defer this.deref() without a matching this.ref(). Either add a ref at entry or remove the unmatched defer.

-    pub fn onValkeyClose(this: *JSValkeyClient) void {
-        const globalObject = this.globalObject;
-        this.poll_ref.disable();
-        defer this.deref();
+    pub fn onValkeyClose(this: *JSValkeyClient) void {
+        const globalObject = this.globalObject;
+        this.poll_ref.disable();
+        this.ref();
+        defer this.deref();
♻️ Duplicate comments (9)
test/js/valkey/valkey.test.ts (2)

215-219: Close clients created by connectedRedis to avoid leaks.

Most tests call connectedRedis() but never close the returned clients. Add try/finally around each test body to call redis.close()/subscriber.close(), or track them and close in afterEach. This prevents dangling connections in CI.

Example:

-    const redis = await connectedRedis();
+    const redis = await connectedRedis();
+    try {
+      // test body...
+    } finally {
+      redis.close();
+    }

221-230: Replace sleeps and async forEach with subscriber-side completion signals (deterministic, no flakiness).

  • async forEach(...) isn’t awaited.
  • sleep(flushTimeoutMs) is a race against server→subscriber delivery.

Wait for the subscriber to observe the expected number of messages and use a bounded timeout. Apply this pattern across these tests.

Minimal helper you can add near the top (outside diffed ranges):

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any;
  return Promise.race([
    p,
    new Promise<T>((_, rej) => (t = setTimeout(() => rej(new Error(`timeout after ${ms}ms`)), ms))),
  ]).finally(() => clearTimeout(t));
}

Concrete fix for “subscribing to a channel receives messages”:

-      var receiveCount = 0;
-      await subscriber.subscribe(testChannel, (message, channel) => {
-        receiveCount++;
-        expect(channel).toBe(testChannel);
-        expect(message).toBe(testMessage);
-      });
-
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
-
-      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);
+      let receiveCount = 0;
+      const done = Promise.withResolvers<void>();
+      await subscriber.subscribe(testChannel, (message, channel) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
+      });
+
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+
+      await withTimeout(done.promise, 3000);
+      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);

Apply analogous changes to:

  • “messages are received in order”: accumulate sent/received, resolve when counts match, then assert equality (no sleep).
  • Multi-channel test: track per-channel counts and resolve when each channel’s received length equals sent length.
  • Unsubscribe sequencing tests: resolve after pre-unsubscribe deliveries before calling unsubscribe; resolve after post-unsubscribe deliveries.
  • Duplicate listeners, empty messages, special channel names, publish while subscribed, callback error resilience, listener unsubscription: replace sleeps with a done.resolve() triggered by expected counters.

Also applies to: 232-241, 243-251, 252-274, 276-300, 302-341, 342-381, 383-413, 414-432, 433-457, 459-474, 475-501, 502-521, 523-530, 531-537, 538-565, 566-574, 575-617

src/valkey/valkey.zig (2)

390-396: Debug print of enum is fine; no need for @Errorname.

Using "{}" with an enum logs the tag; this supersedes earlier guidance to use @TagName with "{s}".


643-651: Fix promise resolution: calling promise_ptr.promise.resolve is invalid.

promise_ptr is already a *Command.Promise. Use promise_ptr.resolve(...). Same for the unsubscribe branch.

-                    this.onValkeySubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
+                    this.onValkeySubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
                     return;
                 } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                    this.onValkeyUnsubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.js_undefined);
+                    this.onValkeyUnsubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.js_undefined);
                     return;
src/valkey/js_valkey.zig (3)

111-121: Treat null the same as undefined when initializing handler arrays.

-            if (existing_handler_arr.isUndefined()) {
+            if (existing_handler_arr.isUndefinedOrNull()) {
                 // Create a new array if the existing_handler_arr is undefined/null
                 handlers_array = try jsc.JSArray.createEmpty(globalObject, 0);

748-755: Don’t tear down the client when a subscriber callback throws.

Failing with failWithJSValue() converts application-level errors into connection failures; the “callback errors don't crash the client” test expects the opposite.

-        ) catch |e| {
-            this.debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
-        };
+        ) catch |e| {
+            this.debug("Subscriber callback threw: {}", .{e});
+            globalObject.reportActiveExceptionAsUnhandled(e);
+        };

846-851: Double deref in finalize.

You already have defer this.deref() at Line 837; the explicit this.deref() at Line 851 double-decrements.

-        if (this.subscription_ctx) |*subscription_ctx| {
+        if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();
src/valkey/js_valkey_functions.zig (2)

701-769: subscribe(): missing async cleanup on Promise rejection (can leave client “stuck” in subscriber mode)

If send() later rejects, subscription_ctx is never cleaned up. Attach a rejection handler.

-    const promise = this.send(
+    const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         // If we find an error, we need to clean up the subscription context.
         this.deleteSubscriptionCtx();
         return protocol.valkeyErrorToJS(globalObject, "Failed to send SUBSCRIBE command", err);
     };
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    // Ensure cleanup if the subscribe promise rejects asynchronously.
+    cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

Note: onSubscribeReject() should deleteSubscriptionCtx() and rethrow.


653-699: publish(): add subscriber-mode guard and fix message type error text

  • Missing guard: publishing from subscriber mode should throw (consistent with other commands).
  • Error text says “string or buffer or number” but you already require a string via isString().

Apply:

 pub fn publish(
@@
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (3)
test/js/valkey/valkey.test.ts (1)

553-554: Remove console.log noise from tests.

Logs make CI output noisy; rely on assertions only.

-      console.log("Publishing messages to channel with error callback");
+      // Publishing messages to channel with error callback
...
-      console.log("Unsubscribing from channel after error test");
+      // Unsubscribe after verifying callback error is handled

Also applies to: 562-563

src/valkey/js_valkey.zig (2)

686-712: onValkeySubscribe/Unsubscribe stubs are fine; consider logging counters.

Optional: log current subscriptionCount() in debug builds for easier diagnostics.


1193-1193: Visibility for RedisJS logs is currently .visible.

If too chatty in production, consider gating on an option or using .debug visibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3ac3d169af6562acd530438bc1cf57a926c4b2 and 906e20ea2d822c6d6c149561d0e5616c9ea9fcce.

📒 Files selected for processing (5)
  • packages/bun-types/redis.d.ts (2 hunks)
  • src/valkey/js_valkey.zig (12 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (15 hunks)
  • test/js/valkey/valkey.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bun-types/redis.d.ts
🧰 Additional context used
📓 Path-based instructions (11)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
🧠 Learnings (16)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • src/valkey/js_valkey.zig
  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:07:54.573Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`

Applied to files:

  • src/valkey/js_valkey.zig
  • src/valkey/valkey.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🔇 Additional comments (8)
src/valkey/valkey.zig (1)

174-183: Scoped logger usage looks good and consistent.

Also applies to: 1094-1098

src/valkey/js_valkey.zig (4)

355-366: Per-client debug/identifier helpers are useful and scoped; OK to keep under debug flag.

Also applies to: 368-372


306-353: SubscriptionCtx lifecycle toggles (flags) look correct and restore original settings.

Also applies to: 373-406


1067-1074: Handshake logging is fine; keep it behind the RedisJS scope.


863-909: Connection setup and context reuse look correct; good use of uWS contexts.

src/valkey/js_valkey_functions.zig (3)

44-46: Subscriber-mode guards before regular commands — LGTM

Consistent, correct early-exit guards to prevent non-pub/sub commands while in subscriber mode.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559, 913-916


473-485: HMGET/HMSET: confirm slice lifetime semantics

You pass jsc.ZigString.Slice buffers via .slices; ensure send() copies them synchronously before these slices are deinit’d. If not guaranteed, switch to JSArgument-based args for safe ownership.

Also applies to: 607-619


231-246: EXPIRE: mixed raw/slice args — verify ownership

.raw uses key.slice() and a stack buffer (seconds_slice). Confirm Command copies these bytes before return; otherwise, convert seconds to a JSArgument and use .args = .{ .args = &.{key, seconds_arg} } for safer lifetimes.

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
test/js/valkey/valkey.test.ts (1)

188-191: Use rejects for async error assertions.

toThrow* expects sync functions; use rejects with the promise.

-      expect(async () => {
-        await customRedis.get("test");
-      }).toThrowErrorMatchingInlineSnapshot(`"WRONGPASS invalid username-password pair or user is disabled."`);
+      await expect(customRedis.get("test"))
+        .rejects.toThrow("WRONGPASS invalid username-password pair or user is disabled.");

Also close the client in finally:

try {
  // assertion
} finally {
  customRedis.close();
}
src/valkey/js_valkey.zig (1)

792-801: onValkeyClose: ref/deref imbalance — potential underflow

There’s a defer this.deref() (Line 795) without a matching this.ref(), and then another ref()/deref() pair (Lines 799–801). Net -1.

Apply either:

  • Option A (preferred): single balanced pair at top.
-        this.poll_ref.disable();
-        defer this.deref();
+        this.poll_ref.disable();
+        this.ref();
+        defer this.deref();
@@
-        this.ref();
-        defer this.deref();
+        // second ref/deref not needed; kept alive by the pair above
  • Option B: keep both defers but add an initial this.ref() before Line 795.
♻️ Duplicate comments (11)
test/js/valkey/valkey.test.ts (4)

21-25: Don’t hardcode Redis URL; ensure clients are closed in tests.

Use DEFAULT_REDIS_URL so CI/env-config works, and close connections to avoid leaks.

-  const connectedRedis = async () => {
-    const redis = new RedisClient("redis://localhost:6379");
-    await redis.connect();
-    return redis;
-  };
+  const connectedRedis = async () => {
+    const redis = new RedisClient(DEFAULT_REDIS_URL);
+    await redis.connect();
+    return redis;
+  };

Example cleanup pattern to apply in tests that call connectedRedis():

const redis = await connectedRedis();
try {
  // ...test...
} finally {
  redis.close();
}

249-250: Await .resolves; otherwise the assertion may be skipped.

Add await so failures are observed.

-      expect(redis.set(testKey, testValue)).resolves.toEqual("OK");
+      await expect(redis.set(testKey, testValue)).resolves.toBe("OK");

264-271: Remove async-forEach and arbitrary sleeps; wait on subscriber-side conditions.

forEach(async ...) isn’t awaited and sleep(...) is flaky. Resolve a Promise in the subscription callback when expected counts are reached, and await it with a timeout.

Representative fix for “subscribing to a channel receives messages”:

-      var receiveCount = 0;
-      await subscriber.subscribe(testChannel, (message, channel) => {
-        receiveCount++;
-        expect(channel).toBe(testChannel);
-        expect(message).toBe(testMessage);
-      });
-
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
-
-      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);
+      let receiveCount = 0;
+      const done = Promise.withResolvers<void>();
+      await subscriber.subscribe(testChannel, (message, channel) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
+      });
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+      await withTimeout(done.promise, 3000);
+      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);

Apply the same done+withTimeout pattern to the other cited locations.

Add this helper near the top of the file:

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any;
  return Promise.race([
    p,
    new Promise<T>((_, rej) => (t = setTimeout(() => rej(new Error(`timeout after ${ms}ms`)), ms))),
  ]).finally(() => clearTimeout(t));
}

Also applies to: 287-295, 327-328, 363-364, 373-374, 405-406, 425-426, 491-492, 558-559, 595-596, 604-605, 613-614


221-231: Close Redis clients in all Pub/Sub tests
Wrap every connectedRedis() invocation in a try/finally, unsubscribe if subscribed, then call client.close(). For example, in test/js/valkey/valkey.test.ts around the tests beginning at lines 221, 232, 252, etc., ensure each redis and subscriber is closed to prevent resource leaks.

src/valkey/js_valkey.zig (4)

850-868: Double deref in finalize() — remove the explicit call

You already ref()/defer deref() at the top; the extra this.deref() at Line 868 double-decrements.

         this.client.flags.finalized = true;
         this.client.close();
         if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();

63-100: removeReceiveHandler(): return contract mismatch, unsafe array handling, and wrong JS identity check

  • Return null when the channel isn’t registered; current code returns 0 (Line 65).
  • Don’t call arrayIterator() before verifying the value is an array; can throw (Lines 68–72).
  • Compare callbacks via strict JS identity, not Zig “==” (Line 80).
  • Optional: drop the per-item debug log; it’s noisy in hot paths (Line 86).

Apply:

@@
-        if (existing == null) {
-            // Could not find the channel, nothing to remove.
-            return 0;
-        }
-
-        const existing_count = (try existing.?.arrayIterator(globalObject)).len;
-        if (existing.?.isUndefinedOrNull()) {
-            // Nothing to remove.
-            return existing_count;
-        }
+        if (existing == null or existing.?.isUndefinedOrNull()) {
+            // Channel not registered
+            return null;
+        }
+        if (!existing.?.isArray()) {
+            // Not an array – treat as no handlers registered
+            return null;
+        }
@@
-        while (try array_it.next()) |iter| {
-            if (iter == callback) {
+        while (try array_it.next()) |iter| {
+            if (iter.strictEquals(globalObject, callback)) {
                 continue;
             }
 
             try updated_array.push(globalObject, iter);
-
-            debug("Added an element to updated_array. Current length={}", .{(try updated_array.arrayIterator(globalObject)).len});
         }
@@
-        const new_length = (updated_array.arrayIterator(globalObject) catch unreachable).len;
+        const new_length = (updated_array.arrayIterator(globalObject) catch unreachable).len;
         if (new_length != 0) {
             map.set(globalObject, channelName, updated_array);
         }
 
         return new_length;

111-121: Treat null like undefined when initializing handler arrays

Use isUndefinedOrNull() so null doesn’t hit the unreachable branch. Aligns with prior feedback.

-            if (existing_handler_arr.isUndefined()) {
+            if (existing_handler_arr.isUndefinedOrNull()) {
                 // Create a new array if the existing_handler_arr is undefined/null
                 handlers_array = try jsc.JSArray.createEmpty(globalObject, 0);
             } else if (existing_handler_arr.isArray()) {
                 // Use the existing array
                 handlers_array = existing_handler_arr;
             } else unreachable;

765-772: Don’t tear down the connection if a subscriber callback throws

Catching and calling failWithJSValue() converts user callback errors into connection failures.

-        ) catch |e| {
-            debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
-        };
+        ) catch |e| {
+            debug("Subscriber callback threw: {}", .{e});
+            // Report, but keep the connection alive.
+            globalObject.reportActiveExceptionAsUnhandled(e);
+        };
src/valkey/js_valkey_functions.zig (3)

1215-1217: Expose PSUBSCRIBE/PUNSUBSCRIBE only if pattern messages are handled

The code exports psubscribe/punsubscribe, but there’s no pmessage handling wired. Either implement pmessage dispatch or gate/remove these exports to avoid confusing users.

- pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
- pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
- pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB").call;
+ // TODO: enable when pmessage handling is implemented end-to-end
+ // pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
+ // pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
+ pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB").call;

To confirm:

#!/bin/bash
# Verify pmessage handler presence (expected: none)
rg -nP '\bpmessage\b|\bonValkeyPMessage\b' src/valkey

857-875: unsubscribe(channel, listener): sending UNSUBSCRIBE with zero args unsubscribes ALL

When remaining_listeners == 0 you pass an empty args list. Append the specific channel so only that channel is unsubscribed.

-        if (remaining_listeners != null and remaining_listeners.? == 0) {
-            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
-        }
+        if (remaining_listeners != null and remaining_listeners.? == 0) {
+            const chanArg = (try fromJS(globalObject, channel)) orelse unreachable;
+            redis_channels.appendAssumeCapacity(chanArg);
+            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
+        }

653-699: publish(): add subscriber-mode guard and fix message type error text

  • In subscriber mode, Redis/Valkey disallow normal commands.
  • The error string says “string or buffer or number” but you only allow strings.
 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
     const args_view = callframe.arguments();
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }

If you intend to support Buffer/number, allow them in the check and align the d.ts instead.

🧹 Nitpick comments (10)
test/js/valkey/valkey.test.ts (2)

633-647: Remove arbitrary sleep and stray logging in duplicate() test; assert readiness deterministically and close both clients.

-      await sleep(2000);
-
-      const duplicate = await redis.duplicate();
-      console.log(duplicate);
+      const duplicate = await redis.duplicate();
+      await expect(duplicate.ping()).resolves.toBe("PONG");
@@
-      duplicate.close();
+      duplicate.close();
+      redis.close();

553-563: Remove console.log noise from tests.

-      console.log("Publishing messages to channel with error callback");
@@
-      console.log("Unsubscribing from channel after error test");
packages/bun-types/redis.d.ts (3)

680-684: Unify wording with single-channel variant.

-     * @note Subscribing moves the channels to a dedicated subscription state
-     *       in which only a limited set of commands can be executed.
+     * @note Subscribing switches the connection into Pub/Sub mode with limited
+     *       command availability (same semantics as single-channel subscribe()).

719-724: Tighten unsubscribe() notes for clarity and consistency.

-     * @note Unsubscribing moves the channel back to a normal state out of the
-     *       subscription state if all channels have been unsubscribed from.
-     *       For further details on the subscription state, @see subscribe.
+     * @note If this removes the last subscription, the connection exits Pub/Sub
+     *       mode and automatic pipelining (if previously enabled) is re-enabled.
+     *       See subscribe() for details about Pub/Sub mode.

Also applies to: 732-737


2-2: Optional: export SubscriptionListener for consumers.

-  type SubscriptionListener = (message: string, channel: string) => void;
+  export type SubscriptionListener = (message: string, channel: string) => void;
src/valkey/valkey.zig (1)

1087-1087: Remove duplicate import alias; use Command consistently.

-const ValkeyCommand = @import("./ValkeyCommand.zig");

And update any remaining ValkeyCommand.* references to Command.* (see handleSubscribeResponse).

src/valkey/js_valkey.zig (3)

881-882: Nit: log message says “Redis” while this module is Valkey

Prefer consistent naming in logs.

-        debug("Connecting to Redis.", .{});
+        debug("Connecting to Valkey.", .{});

28-30: Remove stale TODO; implementation looks correct

subscriptionCount() delegates to JSMap.size(), which is fine. The TODO is misleading.

-        // TODO(markovejnovic): Implement this correctly.
         return this.subscriptionCallbackMap().size(globalObject);

1211-1211: Scope name nit: RedisJS → ValkeyJS

Minor naming consistency with the surrounding module.

-const debug = bun.Output.scoped(.RedisJS, .visible);
+const debug = bun.Output.scoped(.ValkeyJS, .visible);
src/valkey/js_valkey_functions.zig (1)

556-619: HMSET is deprecated; consider HSET for multi-field updates

Prefer HSET (supports variadic field/value pairs) for modern Redis/Valkey. Keep HMSET as alias if needed for compatibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 906e20ea2d822c6d6c149561d0e5616c9ea9fcce and 38b2033dc67cba044559cd90365a6ce01ff4b213.

📒 Files selected for processing (6)
  • packages/bun-types/redis.d.ts (2 hunks)
  • src/bun.js/api/valkey.classes.ts (1 hunks)
  • src/valkey/js_valkey.zig (7 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (6 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/bun.js/api/valkey.classes.ts
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (15)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • packages/bun-types/redis.d.ts
  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
packages/bun-types/redis.d.ts (2)
  • redis (758-758)
  • RedisClient (59-747)
test/js/valkey/test-utils.ts (2)
  • randomCoinFlip (787-789)
  • DEFAULT_REDIS_URL (113-113)
🔇 Additional comments (3)
src/valkey/valkey.zig (3)

1047-1050: cparent() accessor LGTM.


382-382: Good: error logging now prints enum value via formatter.


611-659: Verify all promise resolve/reject call sites

  • Inspect .promise.resolve/.promise.reject at src/valkey/valkey.zig:192, 340, 635, 639 and src/valkey/ValkeyCommand.zig:137, 141, 157 to ensure each in-flight command is popped before resolving or rejecting.
  • Note: no in_flight.readItem() calls were found prior to .Push handling elsewhere.

Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread src/valkey/valkey.zig Outdated
Comment thread src/valkey/valkey.zig Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
src/valkey/valkey.zig (2)

622-670: Fix promise resolution: calling promise_ptr.promise.resolve is invalid; also unify type to Command.PromisePair

promise_ptr is already *Command.Promise. Dereferencing .promise will not compile. Also, use the same imported alias (Command) consistently.

Apply:

-fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *ValkeyCommand.PromisePair) ?void {
+fn handleSubscribeResponse(this: *ValkeyClient, value: *protocol.RESPValue, pair: *Command.PromisePair) ?void {
     // Resolve the promise with the potentially transformed value
     var promise_ptr = &pair.promise;
@@
-                if (std.mem.eql(u8, push.kind, "subscribe")) {
-                    this.onValkeySubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
-                    return;
-                } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                    this.onValkeyUnsubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.js_undefined);
-                    return;
+                if (std.mem.eql(u8, push.kind, "subscribe")) {
+                    this.onValkeySubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
+                    return;
+                } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
+                    this.onValkeyUnsubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.js_undefined);
+                    return;
                 } else {

737-793: Do not pop in-flight promise before classifying the incoming value (drops promises on push messages)

readItem() consumes the next promise even for unrelated .Push packets; early returns then permanently drop it. Defer reading until you know a promise is needed.

Apply:

-        // Let's load the promise pair.
-        var pair_maybe = this.in_flight.readItem();
+        // Defer loading the promise pair until needed.
+        var pair_maybe: ?Command.PromisePair = null;
@@
-        if (this.cparent().isSubscriber()) {
+        if (this.cparent().isSubscriber()) {
             debug("This client is a subscriber. Handling as subscriber...", .{});
@@
-            if (pair_maybe) |*pair| {
-                debug("There is a request in flight. Handling as a subscribe request...", .{});
-                const res = this.handleSubscribeResponse(value, pair);
-                if (res != null) {
-                    return;
-                }
-            }
+            // Only consume a promise when the incoming value requires it.
@@
-                .Push => |push| {
+                .Push => |push| {
                     if (std.mem.eql(u8, push.kind, "message")) {
                         @branchHint(.likely);
                         debug("Received a message.", .{});
                         this.onValkeyMessage(push.data);
                         return;
-                    } else if (std.mem.eql(u8, push.kind, "subscribe")) {
-                        @branchHint(.cold);
-                        debug("Received subscription message without promise: {any}", .{push.data});
-                        return;
-                    } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                        @branchHint(.cold);
-                        debug("Received unsubscribe message without promise: {any}", .{push.data});
-                        return;
+                    } else if (std.mem.eql(u8, push.kind, "subscribe") or std.mem.eql(u8, push.kind, "unsubscribe")) {
+                        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+                        if (pair_maybe) |*pair| {
+                            _ = this.handleSubscribeResponse(value, pair) orelse {};
+                        } else {
+                            debug("No in-flight promise for subscription push", .{});
+                        }
+                        return;
                     } else {
                         @branchHint(.cold);
                         this.fail("Unexpected push message kind without promise", protocol.RedisError.InvalidResponseType);
                         return;
                     }
                 },
                 else => {
                     // In the else case, fall through to regular handler
                 },
             }
 
             debug("Treating subscriber response as a regular command...", .{});
         }
 
         // For regular commands, get the next command+promise pair from the queue
-        var pair = pair_maybe orelse {
+        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+        var pair = pair_maybe orelse {
             debug("Received response but no promise in queue", .{});
             return;
         };
src/valkey/js_valkey_functions.zig (3)

701-769: subscribe(): attach rejection handler to clean up subscription_ctx on async failure

If the returned Promise rejects later, the client remains “stuck” in subscriber mode. Attach a .then with a rejection callback that calls deleteSubscriptionCtx().

Apply:

     const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         // If we find an error, we need to clean up the subscription context.
         this.deleteSubscriptionCtx();
         return protocol.valkeyErrorToJS(globalObject, "Failed to send SUBSCRIBE command", err);
     };
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    // Ensure cleanup if subscribe fails asynchronously.
+    cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

Assumes onSubscribeReject() exists and deletes the ctx before rethrowing. If not, I can provide it.


653-699: publish(): add subscriber-mode guard and fix type error text to match validation

  • Publishing while subscribed should be blocked (matches Redis semantics).
  • You validate arg1 as string, but the error says “string or buffer or number”.

Apply:

 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
     const args_view = callframe.arguments();
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }

807-910: unsubscribe(channel, listener): avoid unsubscribing ALL; fix resolved undefined; attach deferred cleanup

  • When remaining_listeners == 0, you send UNSUBSCRIBE with empty args (unsubscribes ALL). Append the channel.
  • Resolve to proper JS undefined.
  • Mirror the deferred cleanup behavior.

Apply:

         const remaining_listeners = subscription_ctx.removeReceiveHandler(globalObject, channel, listener_cb) catch {
@@
-        if (remaining_listeners != null and remaining_listeners.? == 0) {
-            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
-        }
+        if (remaining_listeners != null and remaining_listeners.? == 0) {
+            // Unsubscribe this specific channel
+            const chanArg = (try fromJS(globalObject, channel)) orelse unreachable;
+            try redis_channels.append(chanArg);
+            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
+        }
@@
-        const promise = jsc.JSPromise.create(globalObject);
-        promise.resolve(globalObject, .js_undefined);
-        return promise.toJS();
+        const promise = jsc.JSPromise.create(globalObject);
+        promise.resolve(globalObject, jsc.JSValue.jsUndefined());
+        return promise.toJS();
🧹 Nitpick comments (1)
src/valkey/js_valkey_functions.zig (1)

1215-1217: PSUBSCRIBE/PUNSUBSCRIBE exports: gate or implement pmessage handling

If pattern messages aren’t fully wired (no pmessage dispatch), exposing these is misleading. Either:

  • implement pmessage and route to a handler, or
  • feature-gate/remove these exports for now.

I can wire pmessage into the existing dispatch with minimal changes and add tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 38b2033dc67cba044559cd90365a6ce01ff4b213 and 43f576c3cd1bff4464a6b528af3833c4c7b4dd07.

📒 Files selected for processing (4)
  • src/valkey/js_valkey.zig (9 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (7 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/valkey/js_valkey.zig
  • test/js/valkey/valkey.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (4)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (4)
src/valkey/valkey.zig (3)

18-27: Good flag to control duplicate().connect() resolution semantics

The connection_promise_returns_client flag is clear and scoped; docs explain both code paths well.


1058-1061: Const-parent accessor LGTM

Using a const variant avoids unnecessary casts and clarifies readonly access.


1070-1080: Clean public-API bridging for subscribe/unsubscribe/message

Forwarders are simple and correctly typed; good separation of internal vs JS surface.

src/valkey/js_valkey_functions.zig (1)

44-46: Consistent subscriber-mode guards — nice

Blocking non-pub/sub commands while in subscriber mode matches Redis behavior and prevents protocol misuse.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559, 912-919

Comment thread src/valkey/js_valkey_functions.zig Outdated

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/valkey/valkey.test.ts (1)

188-191: Fix async error assertion; use rejects.

Current pattern won’t fail the test on rejection.

-      expect(async () => {
-        await customRedis.get("test");
-      }).toThrowErrorMatchingInlineSnapshot(`"WRONGPASS invalid username-password pair or user is disabled."`);
+      await expect(customRedis.get("test")).rejects.toThrowErrorMatchingInlineSnapshot(
+        `"WRONGPASS invalid username-password pair or user is disabled."`
+      );
♻️ Duplicate comments (14)
test/js/valkey/valkey.test.ts (4)

21-25: Don’t hardcode Redis URL; use DEFAULT_REDIS_URL and ensure clients are closed.

Use the harness URL so CI passes. Also make tests close created clients (redis/subscriber) in finally/afterEach to avoid connection leaks.

-  const connectedRedis = async () => {
-    const redis = new RedisClient("redis://localhost:6379");
-    await redis.connect();
-    return redis;
-  };
+  const connectedRedis = async () => {
+    const redis = new RedisClient(DEFAULT_REDIS_URL);
+    await redis.connect();
+    return redis;
+  };

Option: track clients and add afterEach(() => client.close?.()).


249-250: Await the .resolves assertion.

-      expect(redis.set(testKey, testValue)).resolves.toEqual("OK");
+      await expect(redis.set(testKey, testValue)).resolves.toBe("OK");

264-271: Remove async-forEach and arbitrary sleeps; wait for delivery conditions.

Use a resolver that completes when the subscriber observes the expected count (with a bounded timeout). Apply this pattern across the listed ranges.

Example for 264–271:

-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      const done = Promise.withResolvers<void>();
+      const TARGET = TEST_MESSAGE_COUNT;
+      // augment existing subscriber callback:
+      const original = (message: string, channel: string) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TARGET) done.resolve();
+      };
+      await subscriber.unsubscribe(testChannel);
+      await subscriber.subscribe(testChannel, original);
+      for (let i = 0; i < TARGET; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+      await withTimeout(done.promise, 3000);

Add once near top of file:

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any;
  return Promise.race([p, new Promise<T>((_, r) => (t = setTimeout(() => r(new Error(`timeout after ${ms}ms`)), ms)))])
    .finally(() => clearTimeout(t));
}

Also applies to: 287-295, 326-328, 363-364, 373-374, 405-406, 425-426, 491-492, 558-559, 591-601, 609-610


531-536: Assert async rejection for unsubscribe outside subscriber mode.

-      expect(() => redis.unsubscribe(channel)).toThrow("Not in subscription mode");
+      await expect(redis.unsubscribe(channel)).rejects.toThrow("Not in subscription mode");
src/valkey/js_valkey.zig (5)

111-121: Treat null like undefined when initializing handler arrays.

-            if (existing_handler_arr.isUndefined()) {
+            if (existing_handler_arr.isUndefinedOrNull()) {

1053-1057: PSUBSCRIBE/PUNSUBSCRIBE exposed but no pmessage dispatch.

Either wire pattern-message handling (onValkeyPMessage) or gate/remove these exports to avoid a broken surface.

-    pub const psubscribe = fns.psubscribe;
-    pub const punsubscribe = fns.punsubscribe;
+    // TODO: enable when pmessage handling is implemented
+    // pub const psubscribe = fns.psubscribe;
+    // pub const punsubscribe = fns.punsubscribe;

63-72: removeReceiveHandler: return null for missing channel; fix array checks and JS identity compare.

Contract says “null if channel not registered,” but code returns 0; it also iterates before verifying array type and compares callbacks with Zig equality.

-        if (existing == null) {
-            // Could not find the channel, nothing to remove.
-            return 0;
-        }
-
-        const existing_count = (try existing.?.arrayIterator(globalObject)).len;
-        if (existing.?.isUndefinedOrNull()) {
-            // Nothing to remove.
-            return existing_count;
-        }
+        if (existing == null || existing.?.isUndefinedOrNull()) {
+            return null;
+        }
+        if (!existing.?.isArray()) {
+            return null;
+        }
+        var array_it = try existing.?.arrayIterator(globalObject);
@@
-            if (iter == callback) {
+            if (iter.strictEquals(globalObject, callback)) {
                 continue;
             }

Also applies to: 74-87, 93-100


884-889: Double deref in finalize().

There’s a deferred deref already; the explicit deref at the end double-decrements.

-        if (this.subscription_ctx) |*subscription_ctx| {
+        if (this.subscription_ctx) |*subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();

786-794: Don’t close the client when a subscriber callback throws.

Callback exceptions should be reported, not tear down the connection.

-        ) catch |e| {
-            debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
-        };
+        ) catch |e| {
+            debug("Subscriber callback threw: {}", .{e});
+            globalObject.reportActiveExceptionAsUnhandled(e);
+        };
src/valkey/js_valkey_functions.zig (5)

754-769: subscribe(): cleanup ctx on async rejection.

If the send() promise later rejects, client stays stuck in subscriber mode.

-    const promise = this.send(
+    const promise = this.send(
         globalObject,
         callframe.this(),
         &command,
     ) catch |err| {
         // If we find an error, we need to clean up the subscription context.
         this.deleteSubscriptionCtx();
         return protocol.valkeyErrorToJS(globalObject, "Failed to send SUBSCRIBE command", err);
     };
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    // Ensure cleanup if subscribe fails asynchronously.
+    cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

Add (outside diff range) an instance method:

pub fn onSubscribeReject(this: *JSValkeyClient) jsc.JSValue {
    return jsc.JSValue.createFunction(this.globalObject, "onSubscribeReject", struct {
        pub fn call(go: *jsc.JSGlobalObject, _: jsc.CallFrame) jsc.JSValue {
            const self = JSValkeyClient.fromJS(go, go.thisValue()).?;
            self.deleteSubscriptionCtx();
            return jsc.JSValue.getException(go) orelse .js_undefined;
        }
    }.call, .{});
}

797-805: UNSUBSCRIBE cleanup is racy — delete ctx after Promise resolves.

Defer ctx deletion until the UNSUBSCRIBE promise resolves.

-    // Check if we have any remaining subscriptions
-    // If the callback map is empty, we can exit subscription mode
-    if (!subscription_ctx.hasSubscriptions(globalObject)) {
-        // No more subscriptions, exit subscription mode
-        this.deleteSubscriptionCtx();
-    }
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    cpp.JSC__JSValue__then(promise_value, globalObject, this.onMaybeDeleteCtxResolve(), jsc.JSValue.zero());
+    return promise_value;

Add (outside diff range):

pub fn onMaybeDeleteCtxResolve(this: *JSValkeyClient) jsc.JSValue {
    return jsc.JSValue.createFunction(this.globalObject, "onMaybeDeleteCtxResolve", struct {
        pub fn call(go: *jsc.JSGlobalObject, _: jsc.CallFrame) jsc.JSValue {
            const self = JSValkeyClient.fromJS(go, go.thisValue()).?;
            if (self.subscription_ctx) |*ctx| {
                if (!ctx.hasSubscriptions(go)) self.deleteSubscriptionCtx();
            }
            return .js_undefined;
        }
    }.call, .{});
}

866-868: unsubscribe(channel, listener): sending UNSUBSCRIBE with empty args — unsubscribes ALL.

Append the specific channel before calling the helper.

-        if (remaining_listeners != null and remaining_listeners.? == 0) {
-            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
-        }
+        if (remaining_listeners != null and remaining_listeners.? == 0) {
+            const chanArg = (try fromJS(globalObject, channel)) orelse unreachable;
+            redis_channels.appendAssumeCapacity(chanArg);
+            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
+        }

1215-1217: Remove or gate PSUBSCRIBE/PUNSUBSCRIBE until pattern messages are handled.

Avoid exposing incomplete pattern-pubsub. Match js_valkey.zig.

-pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
-pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
+// TODO: enable when pmessage handling is implemented
+// pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
+// pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;

676-684: publish(): align types with d.ts (“message” must be string).

Runtime accepts buffer/number; typings say string. Tighten validation or update types.

-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (2)
src/valkey/js_valkey.zig (1)

342-344: Complete the docstring for cloneWithoutConnecting.

-    /// Clone this client while remaining in the initial disconnected state. This does not preserve
-    /// the
+    /// Clone this client while remaining in the initial disconnected state.
+    /// This does not preserve subscription state or any in-flight commands.
src/valkey/js_valkey_functions.zig (1)

607-618: Prefer HSET over deprecated HMSET.

Use HSET for field/value pairs.

-            .command = "HMSET",
+            .command = "HSET",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 43f576c3cd1bff4464a6b528af3833c4c7b4dd07 and ac6a96b182a4640337bb57e75c9fa1214dba4770.

📒 Files selected for processing (3)
  • src/valkey/js_valkey.zig (9 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (16)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
packages/bun-types/redis.d.ts (2)
  • redis (758-758)
  • RedisClient (59-747)
test/js/valkey/test-utils.ts (2)
  • randomCoinFlip (787-789)
  • DEFAULT_REDIS_URL (113-113)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (1)
test/js/valkey/valkey.test.ts (1)

475-500: Clarify contract: is PUBLISH allowed while subscribed?

Tests assert it works; if that’s the intended API (diverging from Redis), document it in d.ts/docs to avoid confusion. Otherwise, add a guard in publish().

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/valkey/valkey.test.ts (1)

190-193: Fix async assertion: use rejects.

toThrowErrorMatchingInlineSnapshot expects a sync throw; this is a Promise.

-      expect(async () => {
-        await customRedis.get("test");
-      }).toThrowErrorMatchingInlineSnapshot(`"WRONGPASS invalid username-password pair or user is disabled."`);
+      await expect(customRedis.get("test")).rejects.toThrow(
+        "WRONGPASS invalid username-password pair or user is disabled."
+      );
♻️ Duplicate comments (5)
test/js/valkey/valkey.test.ts (4)

13-13: Don’t hardcode Redis URL; use DEFAULT_REDIS_URL and auto-cleanup clients.

Use the harness’ DEFAULT_REDIS_URL and ensure ephemeral clients are closed to avoid CI leaks.

@@
-const DEFAULT_REDIS_URL = "redis://localhost:6379";
@@
-  const connectedRedis = async () => {
-    const redis = new RedisClient("redis://localhost:6379");
+  const connectedRedis = async () => {
+    const redis = new RedisClient(DEFAULT_REDIS_URL);
     await redis.connect();
     return redis;
   };

Add lightweight auto-cleanup (outside the shown range; place after beforeEach):

// helper (outside selected range)
const created: RedisClient[] = [];
afterEach(() => { for (const c of created.splice(0)) c.close?.(); });
const connectedRedis = async () => {
  const r = new RedisClient(DEFAULT_REDIS_URL);
  await r.connect();
  created.push(r);
  return r;
};

Also applies to: 23-27


251-252: Await the .resolves expectation.

-      expect(redis.set(testKey, testValue)).resolves.toEqual("OK");
+      await expect(redis.set(testKey, testValue)).resolves.toBe("OK");

266-272: Replace async forEach + sleeps with event-driven waits.

forEach(async …) isn’t awaited and sleep(flushTimeoutMs) is flaky. Resolve a Promise from the subscriber callback when N messages arrive; await it with a bounded timeout.

Example fix for “subscribing to a channel receives messages”:

@@
-      var receiveCount = 0;
-      await subscriber.subscribe(testChannel, (message, channel) => {
-        receiveCount++;
-        expect(channel).toBe(testChannel);
-        expect(message).toBe(testMessage);
-      });
-
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
-
-      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);
+      let receiveCount = 0;
+      const done = Promise.withResolvers<void>();
+      await subscriber.subscribe(testChannel, (message, channel) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
+      });
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+      // bound wait to avoid flake
+      await Promise.race([done.promise, new Promise((_, r) => setTimeout(() => r(new Error("timeout")), 3000))]);
+      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);

Apply the same pattern to the other indicated locations.

Also applies to: 288-296, 328-330, 365-365, 375-375, 407-407, 426-427, 454-455, 493-494, 555-561, 593-594, 611-612


533-538: Assert async rejection from unsubscribe().

-      expect(() => redis.unsubscribe(channel)).toThrow("Not in subscription mode");
+      await expect(redis.unsubscribe(channel)).rejects.toThrow("Not in subscription mode");
src/valkey/js_valkey_functions.zig (1)

862-873: unsubscribe(channel, listener): avoid UNSUBSCRIBE-all; fix resolved-undefined.

When last listener is removed, you pass empty args, which unsubscribes all. Append the specific channel; also resolve to proper JS undefined.

-        if (remaining_listeners != null and remaining_listeners.? == 0) {
-            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
-        }
+        if (remaining_listeners != null and remaining_listeners.? == 0) {
+            const chanArg = (try fromJS(globalObject, channel)) orelse unreachable;
+            try redis_channels.append(chanArg);
+            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
+        }
@@
-        const promise = jsc.JSPromise.create(globalObject);
-        promise.resolve(globalObject, .js_undefined);
+        const promise = jsc.JSPromise.create(globalObject);
+        promise.resolve(globalObject, jsc.JSValue.jsUndefined());
         return promise.toJS();
🧹 Nitpick comments (1)
test/js/valkey/valkey.test.ts (1)

555-561: Remove console logs; rely on assertions.

Console noise makes CI output noisy; prefer deterministic waits and assertions.

-      console.log("Publishing messages to channel with error callback");
+      // publish a few messages; errors in listener must not crash client
@@
-      console.log("Unsubscribing from channel after error test");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac6a96b182a4640337bb57e75c9fa1214dba4770 and 9253375449d51b5325e0d80a092bf6563c4b16cc.

📒 Files selected for processing (3)
  • src/valkey/js_valkey.zig (9 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/valkey/js_valkey.zig
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (13)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
test/js/valkey/test-utils.ts (2)
  • DEFAULT_REDIS_URL (113-113)
  • randomCoinFlip (787-789)
packages/bun-types/redis.d.ts (2)
  • redis (758-758)
  • RedisClient (59-747)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated

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

Actionable comments posted: 1

♻️ Duplicate comments (9)
src/valkey/js_valkey_functions.zig (4)

653-699: publish(): add subscriber-mode guard and fix message type error text.

Block publish while subscribed and align the error text with string-only.

 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
     const args_view = callframe.arguments();
@@
     const arg1 = callframe.argument(1);
     if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
     }

771-805: UNSUBSCRIBE: defer ctx deletion until promise resolves (avoid races).

Immediate deletion can desync state if the promise rejects or when unsubscribing “all”.

@@
-    // Check if we have any remaining subscriptions
-    // If the callback map is empty, we can exit subscription mode
-    if (!subscription_ctx.hasSubscriptions(globalObject)) {
-        // No more subscriptions, exit subscription mode
-        this.deleteSubscriptionCtx();
-    }
-
-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    if (redis_channels.len == 0) {
+        // Unsubscribe-all: drop ctx after resolve.
+        cpp.JSC__JSValue__then(promise_value, globalObject, this.onUnsubscribeAllResolve(), jsc.JSValue.zero());
+    } else {
+        // Partial: maybe drop ctx after resolve if empty.
+        cpp.JSC__JSValue__then(promise_value, globalObject, this.onMaybeDeleteCtxResolve(), jsc.JSValue.zero());
+    }
+    return promise_value;

Add in JSValkeyClient (src/valkey/js_valkey.zig):

pub fn onUnsubscribeAllResolve(this: *JSValkeyClient) jsc.JSValue {
    return jsc.JSValue.createHostFunction(this.globalObject, "onUnsubAll", 0, struct {
        pub fn call(globalObject: *jsc.JSGlobalObject, thisVal: jsc.JSValue, _: []jsc.JSValue) bun.JSError!jsc.JSValue {
            var self = JSValkeyClient.fromJS(thisVal);
            self.deleteSubscriptionCtx();
            return jsc.JSValue.jsUndefined();
        }
    }.call);
}

pub fn onMaybeDeleteCtxResolve(this: *JSValkeyClient) jsc.JSValue {
    return jsc.JSValue.createHostFunction(this.globalObject, "onMaybeDelCtx", 0, struct {
        pub fn call(globalObject: *jsc.JSGlobalObject, thisVal: jsc.JSValue, _: []jsc.JSValue) bun.JSError!jsc.JSValue {
            var self = JSValkeyClient.fromJS(thisVal);
            if (self.subscriptionCtx()) |ctx| {
                if (!ctx.hasSubscriptions(globalObject)) self.deleteSubscriptionCtx();
            }
            return jsc.JSValue.jsUndefined();
        }
    }.call);
}

841-866: unsubscribe(channel, listener): don’t send UNSUBSCRIBE with empty args.

Append the specific channel when the last listener is removed; otherwise it unsubscribes all.

@@
-        if (remaining_listeners != null and remaining_listeners.? == 0) {
-            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
-        }
+        if (remaining_listeners != null and remaining_listeners.? == 0) {
+            const chanArg = (try fromJS(globalObject, channel)) orelse unreachable;
+            redis_channels.appendAssumeCapacity(chanArg);
+            return try sendUnsubscribeRequestAndCleanup(this, callframe.this(), globalObject, redis_channels.items);
+        }

1215-1218: PSUBSCRIBE/PUNSUBSCRIBE exported without pmessage delivery — remove or gate.

Users won’t receive pattern messages; hide until pmessage path is implemented.

-pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
-pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
 pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB").call;
src/valkey/js_valkey.zig (5)

111-124: upsertReceiveHandler(): treat null like undefined.

Use isUndefinedOrNull() to initialize the array.

-            if (existing_handler_arr.isUndefined()) {
+            if (existing_handler_arr.isUndefinedOrNull()) {

875-893: finalize(): double deref — remove the explicit call.

You already defer a deref at the start.

-        if (this.subscriptionCtx()) |subscription_ctx| {
+        if (this.subscriptionCtx()) |subscription_ctx| {
             // Deinitialize subscription context if it exists
             subscription_ctx.deinit();
         }
-        this.deref();

1067-1072: Hide psubscribe/punsubscribe until pattern delivery is wired.

Mirror the export removal in js_valkey_functions.zig.

-    pub const psubscribe = fns.psubscribe;
@@
-    pub const punsubscribe = fns.punsubscribe;

63-100: removeReceiveHandler(): contract mismatch and identity compare.

Return null when channel not registered; only iterate arrays; compare callbacks via JS identity.

-        if (existing == null) {
-            // Could not find the channel, nothing to remove.
-            return 0;
-        }
-
-        const existing_count = (try existing.?.arrayIterator(globalObject)).len;
-        if (existing.?.isUndefinedOrNull()) {
-            // Nothing to remove.
-            return existing_count;
-        }
+        if (existing == null or existing.?.isUndefinedOrNull()) {
+            return null;
+        }
+        if (!existing.?.isArray()) {
+            return null;
+        }
+        var array_it = try existing.?.arrayIterator(globalObject);
+        const existing_count = array_it.len;
@@
-        while (try array_it.next()) |iter| {
-            if (iter == callback) {
+        while (try array_it.next()) |iter| {
+            if (iter.strictEquals(globalObject, callback)) {
                 continue;
             }

756-801: Don’t tear down connection when a subscriber callback throws.

Report the exception and keep the client alive.

         subs_ctx.invokeCallback(
             globalObject,
             channel_value,
             &[_]JSValue{ message_value, channel_value },
         ) catch |e| {
-            debug("Failed to invoke callbacks. Error: {}", .{e});
-            this.failWithJSValue(globalObject.takeException(e));
+            debug("Subscriber callback threw: {}", .{e});
+            globalObject.reportActiveExceptionAsUnhandled(e);
         };
🧹 Nitpick comments (3)
src/valkey/js_valkey_functions.zig (1)

555-619: hmset(): prefer HSET (HMSET is deprecated).

Keep API name if desired but send HSET on the wire.

-            .command = "HMSET",
+            .command = "HSET",
src/valkey/js_valkey.zig (2)

342-396: cloneWithoutConnecting(): TODO notes about leaks — either dupe or document ownership.

Avoid sharing pointers to address/username/password; dupe or ensure single free.

If you want, I can send a follow-up patch that duplicates these fields and frees them in deinit().


1247-1247: Make RedisJS logs non-visible by default.

Reduce noise; enable at runtime when needed.

-const debug = bun.Output.scoped(.RedisJS, .visible);
+const debug = bun.Output.scoped(.RedisJS, false);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9253375449d51b5325e0d80a092bf6563c4b16cc and a9772d3864bd27676205309c43a5ee0523acaf57.

📒 Files selected for processing (4)
  • src/bun.js/api/valkey.classes.ts (1 hunks)
  • src/valkey/js_valkey.zig (9 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/bun.js/api/valkey.classes.ts
  • test/js/valkey/valkey.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (6)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (2)
src/valkey/js_valkey_functions.zig (2)

44-46: LGTM: subscriber-mode guards added consistently.

These early exits prevent misuse while subscribed.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559


621-651: PING allowed while subscribed — OK.

Not gating PING under subscriber mode matches Redis behavior.

coderabbitai Bot added a commit that referenced this pull request Sep 4, 2025
Docstrings generation was requested by @markovejnovic.

* #21728 (comment)

The following files were modified:

* `src/bun.js/bindings/bindings.cpp`
* `test/js/valkey/test-utils.ts`
@coderabbitai

coderabbitai Bot commented Sep 4, 2025

Copy link
Copy Markdown
Contributor

Note

Generated docstrings for this pull request at #22411

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

Actionable comments posted: 4

♻️ Duplicate comments (9)
src/valkey/js_valkey_functions.zig (1)

653-699: publish(): missing subscriber-mode guard and wrong error text

  • You must block publish in subscriber mode.
  • Error message for message type says “string or buffer or number” but you require a string.
 pub fn publish(
     this: *JSValkeyClient,
     globalObject: *jsc.JSGlobalObject,
     callframe: *jsc.CallFrame,
 ) bun.JSError!JSValue {
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }
     const args_view = callframe.arguments();
@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
test/js/valkey/valkey.test.ts (8)

22-26: Don’t hardcode Redis URL; use harness URL and close clients

Use DEFAULT_REDIS_URL to work in CI; ensure clients are closed in finally blocks.

-  const connectedRedis = async () => {
-    const redis = new RedisClient("redis://localhost:6379");
+  const connectedRedis = async () => {
+    const redis = new RedisClient(DEFAULT_REDIS_URL);
     await redis.connect();
     return redis;
   };

223-231: Publish from non-subscriber

Publishing while subscribed is disallowed; use a separate publisher client.

-  const redis = await connectedRedis();
-  await redis.subscribe(testChannel, () => {});
-  expect(await redis.publish(testChannel, testMessage)).toBe(1);
+  const publisher = await connectedRedis();
+  const subscriber = await connectedRedis();
+  await subscriber.subscribe(testChannel, () => {});
+  expect(await publisher.publish(testChannel, testMessage)).toBe(1);
+  await subscriber.unsubscribe(testChannel);

250-251: Await the resolves matcher

Without await, failures may be missed.

-  expect(redis.set(testKey, testValue)).resolves.toEqual("OK");
+  await expect(redis.set(testKey, testValue)).resolves.toBe("OK");

254-275: Avoid async forEach and arbitrary sleeps; wait for subscriber-side condition

Use a resolver that completes when all messages are observed (with a timeout).

-  var receiveCount = 0;
-  await subscriber.subscribe(testChannel, (message, channel) => {
-    receiveCount++;
-    expect(channel).toBe(testChannel);
-    expect(message).toBe(testMessage);
-  });
-  Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-    expect(await redis.publish(testChannel, testMessage)).toBe(1);
-  });
-  await sleep(flushTimeoutMs);
-  expect(receiveCount).toBe(TEST_MESSAGE_COUNT);
+  let receiveCount = 0;
+  const done = Promise.withResolvers<void>();
+  await subscriber.subscribe(testChannel, (message, channel) => {
+    receiveCount++;
+    expect(channel).toBe(testChannel);
+    expect(message).toBe(testMessage);
+    if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
+  });
+  for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+    expect(await redis.publish(testChannel, testMessage)).toBe(1);
+  }
+  await withTimeout(done.promise, 3000);
+  expect(receiveCount).toBe(TEST_MESSAGE_COUNT);

Helper (add near top of file or in test-utils):

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any; return Promise.race([p, new Promise<T>((_, r)=>t=setTimeout(()=>r(new Error(`timeout ${ms}ms`)), ms))]).finally(()=>clearTimeout(t));
}

Also applies to: 265-271


279-301: Message ordering test: same fix—no async forEach, no sleeps

Switch to awaited loop and wait for subscriber to collect TEST_MESSAGE_COUNT messages.

Also applies to: 287-295


327-328: Remove sleeps; wait for conditions instead

Replace sleeps with event-driven waits (resolver from subscriber callback + timeout).

Also applies to: 374-374, 405-406, 425-426, 452-453, 559-559


476-501: “Publish from a subscribed client” test violates subscriber-mode rule

Publish from a separate non-subscribed client; keep both subscribers to assert return count.

-  // Publishing from the same client should work
-  expect(await redis.publish(channel, "self-published")).toBe(2);
+  const publisher = await connectedRedis();
+  expect(await publisher.publish(channel, "self-published")).toBe(2);

Also applies to: 490-492


532-537: unsubscribe() returns a Promise — assert rejection asynchronously

Use .rejects with await.

-expect(() => redis.unsubscribe(channel)).toThrow("Not in subscription mode");
+await expect(redis.unsubscribe(channel)).rejects.toThrow("Not in subscription mode");
🧹 Nitpick comments (5)
src/deps/uws/us_socket_t.zig (1)

16-16: Align with logging guideline: use log = bun.Output.scoped(..., false)

Codebase guideline prefers const log = bun.Output.scoped(.uws, false); and log("...", .{}). Consider renaming debug → log and using false instead of .visible to avoid noisy default logs.

-const debug = bun.Output.scoped(.uws, .visible);
+const log = bun.Output.scoped(.uws, false);
...
-        debug("us_socket_open({p}, is_client: {})", .{ this, is_client });
+        log("us_socket_open({p}, is_client: {})", .{ this, is_client });

Also applies to: 28-28, 33-33, 38-38, 43-43, 131-131, 139-139, 145-145, 149-149

src/valkey/js_valkey_functions.zig (3)

453-471: HMGET/HMSET: prefer JSArgument over transient ZigString slices

You build slices from temporary strings and deinit them immediately after send; depending on send’s internals, this risks use-after-free. Safer: construct JSArgument values (like other wrappers) and pass .args.

Also applies to: 588-605


115-124: set(): don’t break on first undefined option

Breaking on undefined drops subsequent options; usually we should skip undefined and continue scanning.

-            if (arg.isUndefinedOrNull()) {
-                break;
-            }
+            if (arg.isUndefinedOrNull()) continue;

5-8: jsSend(): simplify array check

isArray() implies object-ness; the isObject() guard is redundant.

-    if (!args_array.isObject() or !args_array.isArray()) {
+    if (!args_array.isArray()) {
         return globalObject.throw("Arguments must be an array", .{});
     }
test/js/valkey/valkey.test.ts (1)

12-12: Remove unused import

c is unused. Drop it to keep tests clean.

-import {c} from "cli/run/cjs-defineProperty-fixture.cjs";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a9772d3864bd27676205309c43a5ee0523acaf57 and 963894b1c732ce98f244971de8c53abb9d5105e7.

📒 Files selected for processing (3)
  • src/deps/uws/us_socket_t.zig (3 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/deps/uws/us_socket_t.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/deps/uws/us_socket_t.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (12)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🔇 Additional comments (3)
src/deps/uws/us_socket_t.zig (1)

16-16: Pointer-based debug logs: LGTM

Switching to {p} with the raw pointer improves clarity and avoids accidental truncation. No behavioral change.

Also applies to: 28-28, 33-33, 38-38, 43-43, 131-131, 139-139, 145-145, 149-149

src/valkey/js_valkey_functions.zig (2)

44-46: Subscriber-mode guards: good coverage

Adding explicit guards to disallow regular commands in subscriber mode matches Redis semantics and prevents confusing runtime errors.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559


1199-1207: duplicate(): unprotect already handled in doConnect resolution
The promise resolution path in js_valkey.zig (unprotect at line 714) unprotects the JS client before resolving, so no additional cleanup is required.

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
test/js/valkey/test-utils.ts (3)

195-205: Fix UNIX socket URL (missing //).

UNIX_REDIS_URL is set to redis+unix:${REDIS_UNIX_SOCKET} in the reuse path; it should include // like other branches or clients may fail to parse.

-        UNIX_REDIS_URL = `redis+unix:${REDIS_UNIX_SOCKET}`;
+        UNIX_REDIS_URL = `redis+unix://${REDIS_UNIX_SOCKET}`;

146-171: Restart helper can’t find the container; align name detection with the started container.

  • startContainer() creates containers named valkey-unified-test-bun-..., but getRedisContainerName() filters docker ps by name=valkey-unified-test, so restartRedisContainer() throws “No Redis container found.”
  • Prefer the already-computed containerConfig.containerName if available, and fall back to docker ps using the valkey-unified-test-bun prefix.
-  const listProcess = Bun.spawn({
-    cmd: [dockerCLI, "ps", "--filter", "name=valkey-unified-test", "--format", "{{.Names}}"],
+  const listProcess = Bun.spawn({
+    cmd: [dockerCLI, "ps", "--filter", "name=valkey-unified-test-bun", "--format", "{{.Names}}"],
     stdout: "pipe",
     env: bunEnv,
   });
-export async function restartRedisContainer(): Promise<void> {
-  const containerName = await getRedisContainerName();
+export async function restartRedisContainer(): Promise<void> {
+  const containerName =
+    containerConfig?.containerName ?? (await getRedisContainerName());

Also applies to: 747-758, 771-793


636-679: Don’t skip cleanup when the primary client is disconnected.

Early-returning if context.redis?.connected is false prevents closing other clients and cleaning keys, leaving resources dangling on CI.

-    if (!context.redis?.connected) {
-      return;
-    }
-
     try {
-      // Clean up Redis keys created during tests
-      const keys = await context.redis.send("KEYS", [`${TEST_KEY_PREFIX}*`]);
-      if (Array.isArray(keys) && keys.length > 0) {
-        // Using del command directly when available
-        if (keys.length === 1) {
-          await context.redis.del(keys[0]);
-        } else {
-          await context.redis.send("DEL", keys);
-        }
-      }
+      // Clean up keys if we can talk to Redis
+      if (context.redis?.connected) {
+        const keys = await context.redis.send("KEYS", [`${TEST_KEY_PREFIX}*`]);
+        if (Array.isArray(keys) && keys.length > 0) {
+          if (keys.length === 1) {
+            await context.redis.del(keys[0]);
+          } else {
+            await context.redis.send("DEL", keys);
+          }
+        }
+      }
 
       // Disconnect all clients
-      await context.redis.close();
+      await context.redis?.close?.();
 
       if (context.redisTLS) {
         await context.redisTLS.close();
       }
@@
       if (context.redisWriteOnly) {
         await context.redisWriteOnly.close();
       }
test/js/valkey/valkey.test.ts (1)

189-192: Connection error test: assert async throw correctly.

Wrapping an async function in expect(() => ...) won’t catch promise rejections.

-      expect(async () => {
-        await customRedis.get("test");
-      }).toThrowErrorMatchingInlineSnapshot(`"WRONGPASS invalid username-password pair or user is disabled."`);
+      await expect(customRedis.get("test")).rejects.toThrowErrorMatchingInlineSnapshot(
+        `"WRONGPASS invalid username-password pair or user is disabled."`,
+      );
♻️ Duplicate comments (14)
test/js/valkey/valkey.test.ts (10)

244-251: Await .resolves so the assertion runs.

Otherwise a rejection won’t fail the test.

-      expect(redis.set(testKey, testValue)).resolves.toEqual("OK");
+      await expect(redis.set(testKey, testValue)).resolves.toBe("OK");

359-382: Unsubscribe assertions: wait for state not time.

-      await sleep(flushTimeoutMs);
+      await withTimeout(
+        (async () => {
+          while (
+            receivedMessages[channel1]?.length !== 2 ||
+            receivedMessages[channel2]?.length !== 1 ||
+            receivedMessages[channel3]?.length !== 2
+          ) await Promise.resolve();
+        })(),
+        3000,
+      );

532-537: Await rejection for async unsubscribe.

-      expect(() => redis.unsubscribe(channel)).toThrow("Not in subscription mode");
+      await expect(redis.unsubscribe(channel)).rejects.toThrow("Not in subscription mode");

539-559: Callback error test: remove sleep and wait for count.

-      await sleep(flushTimeoutMs);
-      expect(messageCount).toBe(3);
+      await withTimeout(
+        (async () => {
+          while (messageCount < 3) await Promise.resolve();
+        })(),
+        3000,
+      );
+      expect(messageCount).toBe(3);

22-26: Don’t hardcode Redis URL; close clients.

Use DEFAULT_REDIS_URL from the harness and ensure callers close the client in finally blocks.

-  const connectedRedis = async () => {
-    const redis = new RedisClient("redis://localhost:6379");
-    await redis.connect();
-    return redis;
-  };
+  const connectedRedis = async () => {
+    const redis = new RedisClient(DEFAULT_REDIS_URL);
+    await redis.connect();
+    return redis;
+  };

253-275: Avoid forEach(async ...) and arbitrary sleeps; wait for delivery.

Use an event-driven resolver on the subscriber side and a bounded timeout.

-      var receiveCount = 0;
-      await subscriber.subscribe(testChannel, (message, channel) => {
-        receiveCount++;
-        expect(channel).toBe(testChannel);
-        expect(message).toBe(testMessage);
-      });
-
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
-
-      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);
+      let receiveCount = 0;
+      const done = Promise.withResolvers<void>();
+      await subscriber.subscribe(testChannel, (message, channel) => {
+        receiveCount++;
+        expect(channel).toBe(testChannel);
+        expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
+      });
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+      await withTimeout(done.promise, 3000);
+      expect(receiveCount).toBe(TEST_MESSAGE_COUNT);

Add once per file (or in test-utils.ts):

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any;
  return Promise.race([p, new Promise<T>((_, r) => (t = setTimeout(() => r(new Error(`timeout after ${ms}ms`)), ms)))])
    .finally(() => clearTimeout(t));
}

277-301: Deterministic order check without sleeps.

-      var receivedMessages: string[] = [];
+      const receivedMessages: string[] = [];
       await subscriber.subscribe(testChannel, message => {
         receivedMessages.push(message);
       });
 
-      var sentMessages: string[] = [];
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        const message = randomUUIDv7();
-        expect(await redis.publish(testChannel, message)).toBe(1);
-        sentMessages.push(message);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      const sentMessages: string[] = [];
+      const done = Promise.withResolvers<void>();
+      (async () => {
+        for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+          const message = randomUUIDv7();
+          sentMessages.push(message);
+          expect(await redis.publish(testChannel, message)).toBe(1);
+        }
+        done.resolve();
+      })();
+      await withTimeout(done.promise, 5000);
+      await withTimeout(
+        (async () => {
+          while (receivedMessages.length < sentMessages.length) await Promise.resolve();
+        })(),
+        5000,
+      );

303-342: Multi-channel test: wait for per-channel counts, not sleep.

-      var receivedMessages: { [channel: string]: string[] } = {};
+      const receivedMessages: { [channel: string]: string[] } = {};
@@
-      var sentMessages: { [channel: string]: string[] } = {};
+      const sentMessages: { [channel: string]: string[] } = {};
@@
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      await withTimeout(
+        (async () => {
+          while (
+            channels.some(ch => (sentMessages[ch]?.length ?? 0) !== (receivedMessages[ch]?.length ?? 0))
+          ) await Promise.resolve();
+        })(),
+        5000,
+      );

476-501: Publishing from a subscribed client — align with server semantics.

Standard Redis forbids PUBLISH from a client in subscriber mode. Use a separate publisher and update the test title.

-    test("publish works from a subscribed client", async () => {
+    test("publish works when publishing from a non-subscriber", async () => {
       const redis = await connectedRedis();
       const channel = "self-publish-channel";
       const subscriber = await connectedRedis();
+      const publisher = await connectedRedis();
@@
-      // Publishing from the same client should work
-      expect(await redis.publish(channel, "self-published")).toBe(2);
+      expect(await publisher.publish(channel, "self-published")).toBe(2);
@@
-      await subscriber.unsubscribe(channel);
-      await redis.unsubscribe(channel);
+      await subscriber.unsubscribe(channel);
+      await redis.unsubscribe(channel);
+      await publisher.close();

222-231: Use a distinct publisher client when publishing to a subscribed channel.

Replace

-      const redis = await connectedRedis();
-
-      await redis.subscribe(testChannel, () => {});
-
-      expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      const publisher = await connectedRedis();
+      const subscriber = await connectedRedis();
+      await subscriber.subscribe(testChannel, () => {});
+      expect(await publisher.publish(testChannel, testMessage)).toBe(1);
+      await subscriber.unsubscribe(testChannel);
+      await Promise.all([publisher.close(), subscriber.close()]);
src/valkey/valkey.zig (2)

737-796: Don’t pop the in-flight promise before classifying the incoming value.

Reading this.in_flight.readItem() up-front drops a promise when early-returning on .Push “message”. Defer reading until needed.

-        // Let's load the promise pair.
-        var pair_maybe = this.in_flight.readItem();
+        // Defer loading the promise pair until needed.
+        var pair_maybe: ?Command.PromisePair = null;
@@
-        if (this.cParent().isSubscriber()) {
+        if (this.cParent().isSubscriber()) {
@@
-            if (pair_maybe) |*pair| {
-                debug("There is a request in flight. Handling as a subscribe request...", .{});
-                const res = this.handleSubscribeResponse(value, pair);
-                if (res != null) {
-                    return;
-                }
-            }
+            if (value.* == .Push and (std.mem.eql(u8, value.Push.kind, "subscribe") or std.mem.eql(u8, value.Push.kind, "unsubscribe"))) {
+                if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+                if (pair_maybe) |*pair| {
+                    _ = this.handleSubscribeResponse(value, pair) orelse {};
+                    return;
+                }
+                debug("No in-flight promise for subscribe/unsubscribe push", .{});
+                return;
+            }
@@
         // For regular commands, get the next command+promise pair from the queue
-        var pair = pair_maybe orelse {
+        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
+        var pair = pair_maybe orelse {
             debug("Received response but no promise in queue", .{});
             return;
         };

622-669: Fix promise resolution: calling .promise.resolve on a *Promise.

promise_ptr is already a *Command.Promise; invoking promise_ptr.promise.resolve won’t compile.

-                    this.onValkeySubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
+                    this.onValkeySubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.jsNumber(sub_count));
                     return;
                 } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                    this.onValkeyUnsubscribe(value);
-                    promise_ptr.promise.resolve(globalThis, jsc.JSValue.js_undefined);
+                    this.onValkeyUnsubscribe(value);
+                    promise_ptr.resolve(globalThis, jsc.JSValue.js_undefined);
                     return;
src/valkey/js_valkey_functions.zig (2)

1230-1232: Gate or implement pattern PUB/SUB.

psubscribe/punsubscribe are exported but pattern pmessage delivery isn’t shown. Either implement pmessage routing or hide these until supported.

-pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE").call;
-pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE").call;
+// TODO: implement pmessage handling before exposing PSUBSCRIBE/PUNSUBSCRIBE
+// pub const psubscribe = ...
+// pub const punsubscribe = ...

653-701: Block publish() in subscriber mode and restrict message to string
In src/valkey/js_valkey_functions.zig, update publish to match other commands’ subscriber-mode guard and align with the d.ts signature:

@@ publish(
-    // Unlike most other commands, publish can be used in subscriber mode.
+    if (this.isSubscriber()) {
+        return globalObject.throw("Cannot use in subscriber mode", .{});
+    }

@@
-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (6)
test/js/valkey/test-utils.ts (3)

161-171: Parsing docker ps --format {{json .}} assumes a single line; make it robust.

If multiple containers match, runningContainers has multiple JSON lines, and JSON.parse(runningContainers) will throw. Parse lines or use --format {{.Names}} and choose the first match.


551-556: Typo.

“by d efault” → “by default”.

-// Initialize test context with TCP client by d efault
+// Initialize test context with TCP client by default

795-800: Tiny style nit: simpler coin flip.

Use Math.random() < 0.5 and === for consistency with the codebase.

-export function randomCoinFlip(): boolean {
-  return Math.floor(Math.random() * 2) == 0;
-}
+export function randomCoinFlip(): boolean {
+  return Math.random() < 0.5;
+}
test/js/valkey/valkey.test.ts (1)

12-12: Remove unused import.

{c} is unused and drags a fixture; drop it.

-import {c} from "cli/run/cjs-defineProperty-fixture.cjs";
src/valkey/valkey.zig (1)

393-412: Use tag name for enum in debug and avoid racy JS in finalizers.

Prefer @tagName(err) for enums to keep logs consistent.

-        debug("failed: {s}: {}", .{ message, err });
+        debug("failed: {s}: {s}", .{ message, @tagName(err) });
src/valkey/js_valkey_functions.zig (1)

807-888: Resolve promises to proper undefined and avoid unsubscribing-all accidentally.

  • You fixed passing the channel for last-listener removal—good.
  • Use jsc.JSValue.jsUndefined() when returning a resolved no-op.
-            const promise = jsc.JSPromise.create(globalObject);
-            promise.resolve(globalObject, .js_undefined);
+            const promise = jsc.JSPromise.create(globalObject);
+            promise.resolve(globalObject, jsc.JSValue.jsUndefined());
             return promise.toJS();
@@
-        const promise = jsc.JSPromise.create(globalObject);
-        promise.resolve(globalObject, .js_undefined);
+        const promise = jsc.JSPromise.create(globalObject);
+        promise.resolve(globalObject, jsc.JSValue.jsUndefined());
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 963894b1c732ce98f244971de8c53abb9d5105e7 and 22d57ad1b78d50a54589026121bfffcd9cb54b79.

📒 Files selected for processing (6)
  • packages/bun-types/redis.d.ts (2 hunks)
  • src/valkey/js_valkey.zig (9 hunks)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • src/valkey/valkey.zig (8 hunks)
  • test/js/valkey/test-utils.ts (2 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/bun-types/redis.d.ts
  • src/valkey/js_valkey.zig
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/valkey.test.ts
  • test/js/valkey/test-utils.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/valkey.test.ts
  • test/js/valkey/test-utils.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/valkey.test.ts
  • test/js/valkey/test-utils.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Tests must be named with the suffix .test.ts or .test.tsx and live under the test/ directory
In tests, always use port: 0 and never hardcode ports or use custom random-port utilities
Prefer snapshot tests using normalizeBunSnapshot(...) over direct string equality on stdout/stderr
Do not write tests that assert absence of 'panic', 'uncaught exception', or similar strings in output
Avoid shelling out to tools like find or grep in tests; use Bun.Glob and built-in utilities

Files:

  • test/js/valkey/valkey.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Prettier to format JS/TS files (bun run prettier)

Files:

  • test/js/valkey/valkey.test.ts
  • test/js/valkey/test-utils.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/valkey.test.ts
  • test/js/valkey/test-utils.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (12)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:09:28.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-03T17:09:28.100Z
Learning: Applies to test/js/bun/** : Place Bun-specific API tests (http, crypto, ffi, shell, etc.) under test/js/bun/

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.460Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.460Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (1)
test/js/valkey/valkey.test.ts (2)
packages/bun-types/redis.d.ts (2)
  • redis (757-757)
  • RedisClient (59-746)
test/js/valkey/test-utils.ts (2)
  • randomCoinFlip (798-800)
  • DEFAULT_REDIS_URL (113-113)

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread test/js/valkey/valkey.test.ts Outdated

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/valkey/js_valkey_functions.zig (1)

447-481: HMGET: field names forced to strings; breaks binary-safe keys/fields.

Key uses JSArgument (binary-safe) but fields are coerced to strings. Use JSArgument for fields too.

Apply:

-    var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1);
+    var args = try std.ArrayList(JSArgument).initCapacity(bun.default_allocator, iter.len + 1);
@@
-    args.appendAssumeCapacity(jsc.ZigString.Slice.fromUTF8NeverFree(key.slice()));
+    args.appendAssumeCapacity(key);
@@
-        const field_str = try field_js.toBunString(globalObject);
-        defer field_str.deref();
-
-        const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator);
-        args.appendAssumeCapacity(field_slice);
+        const field_arg = (try fromJS(globalObject, field_js)) orelse {
+            return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer");
+        };
+        args.appendAssumeCapacity(field_arg);
@@
-            .args = .{ .slices = args.items },
+            .args = .{ .args = args.items },

Don’t forget to deinit() each JSArgument (already handled in the defer loop).

♻️ Duplicate comments (2)
src/valkey/js_valkey_functions.zig (2)

762-773: subscribe(): missing cleanup on async rejection.

If the send() promise later rejects, the subscription ctx remains and the client stays “stuck” in subscriber mode.

Apply:

-    return promise.toJS();
+    const promise_value = promise.toJS();
+    const cpp = bun.cpp;
+    // Ensure cleanup if SUBSCRIBE rejects later.
+    cpp.JSC__JSValue__then(promise_value, globalObject, jsc.JSValue.zero(), this.onSubscribeReject());
+    return promise_value;

If onSubscribeReject() isn’t present, I can provide a minimal helper that calls this.deleteSubscriptionCtx().


681-686: publish(): error text mismatches the enforced type.

You require a string but the error message still says “string or buffer or number”.

Apply:

-    if (!arg1.isString()) {
-        return globalObject.throwInvalidArgumentType(
-            "publish",
-            "message",
-            "string or buffer or number",
-        );
-    }
+    if (!arg1.isString()) {
+        return globalObject.throwInvalidArgumentType("publish", "message", "string");
+    }
🧹 Nitpick comments (4)
src/valkey/js_valkey_functions.zig (4)

799-807: UNSUBSCRIBE: consider deferring ctx deletion until the promise resolves.

Deleting the ctx immediately can desync state if the request later rejects.

Minimal change:

-    if (!subscription_ctx.hasSubscriptions(globalObject)) {
-        this.deleteSubscriptionCtx();
-    }
-
-    return promise.toJS();
+    const value = promise.toJS();
+    if (!subscription_ctx.hasSubscriptions(globalObject)) {
+        const cpp = bun.cpp;
+        cpp.JSC__JSValue__then(value, globalObject, this.onUnsubscribeAllResolve(), jsc.JSValue.zero());
+    }
+    return value;

I can wire onUnsubscribeAllResolve() if needed.


775-777: Typos in comments.

  • “commoand” → “command”
  • “This s important” → “This is important”

Apply:

-/// Send redis the UNSUBSCRIBE RESP command and clean up anything necessary after the unsubscribe commoand.
+/// Send redis the UNSUBSCRIBE RESP command and clean up anything necessary after the unsubscribe command.
-        // unsubscribe from. This s important since this list is used to send
+        // unsubscribe from. This is important since this list is used to send

Also applies to: 859-862


1206-1230: duplicate(): minor safety — ensure unprotect on early error.

If doConnect throws synchronously, new_client_js remains protected.

Apply:

-        new_client.client.flags.connection_promise_returns_client = true;
-        new_client_js.protect();
-        return try new_client.doConnect(globalObject, new_client_js);
+        new_client.client.flags.connection_promise_returns_client = true;
+        new_client_js.protect();
+        return new_client.doConnect(globalObject, new_client_js) catch |err| {
+            new_client_js.unprotect();
+            return err;
+        };

1-41: Nit: jsSend() error label and array check.

Use the public name and a simpler array check.

Apply:

-    if (!args_array.isObject() or !args_array.isArray()) {
+    if (!args_array.isArray()) {
         return globalObject.throw("Arguments must be an array", .{});
     }
@@
-        args.appendAssumeCapacity(try fromJS(globalObject, arg_js) orelse {
-            return globalObject.throwInvalidArgumentType("sendCommand", "argument", "string or buffer");
+        args.appendAssumeCapacity(try fromJS(globalObject, arg_js) orelse {
+            return globalObject.throwInvalidArgumentType("send", "argument", "string or buffer");
         });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 22d57ad1b78d50a54589026121bfffcd9cb54b79 and 1cb3023b37661197d75484e857324bd93b7f9a48.

📒 Files selected for processing (2)
  • src/valkey/js_valkey_functions.zig (19 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/js/valkey/valkey.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup

Files:

  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Run zig-format to format Zig files (bun run zig-format)

Files:

  • src/valkey/js_valkey_functions.zig
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey_functions.zig
🧠 Learnings (3)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:57.965Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:57.965Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🔇 Additional comments (3)
src/valkey/js_valkey_functions.zig (3)

44-46: Subscriber-mode guards look correct and comprehensive.

Consistent with Redis/Valkey semantics. Nice coverage.

Also applies to: 68-70, 92-94, 142-144, 166-168, 190-192, 215-217, 250-252, 275-277, 304-306, 329-331, 354-356, 379-381, 408-410, 438-440, 489-491, 523-525, 557-559, 929-931, 936-938, 943-945, 950-952, 957-959, 964-966, 971-973, 978-980, 985-987, 992-994, 999-1001, 1006-1008, 1013-1015, 1020-1022, 1027-1029, 1034-1036, 1041-1043, 1048-1050, 1055-1057, 1062-1064, 1069-1071, 1076-1078, 1083-1085, 1090-1092, 1096-1098, 1102-1104, 1108-1110, 1114-1116, 1120-1122, 1126-1128, 1132-1134, 1138-1140, 1145-1147, 1151-1153, 1158-1160, 1164-1166, 1170-1172, 1176-1178, 1182-1184, 1188-1190, 1194-1196, 1200-1202


653-703: publish(): overall shape looks good.

Subscriber-mode guard + explicit arg handling is clear and consistent.


561-574: Ensure binary-safe JSArgument usage in HM[SET/GET] and align TypeScript signatures.

  • Replace all ZigString slices with JSArgument in your HMSET/HMGET loops to handle buffers correctly.
  • Manually verify your Redis client’s .d.ts declarations for hmset/hset/hmget match the new varargs or object signature and support binary inputs.

Comment thread src/valkey/js_valkey_functions.zig Outdated
Comment thread src/valkey/js_valkey_functions.zig Outdated

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bun-types/redis.d.ts (1)

128-133: Fix invalid type: Uint8Array is not generic (breaks d.ts compilation).

This will fail type-checking as Uint8Array has no type parameters.

-    getBuffer(key: RedisClient.KeyLike): Promise<Uint8Array<ArrayBuffer> | null>;
+    getBuffer(key: RedisClient.KeyLike): Promise<Uint8Array | null>;
♻️ Duplicate comments (2)
packages/bun-types/redis.d.ts (2)

754-771: Unsubscribe(channel, listener): keep the connection-level note; retain referential-equality guidance.

-     * If there are no more channels subscribed to, the client automatically
-     * re-enables pipelining if it was previously enabled.
-     *
-     * Unsubscribing moves the channel back to a normal state out of the
-     * subscription state if all channels have been unsubscribed from. For
-     * further details on the subscription state, see
-     * {@link subscribe `.subscribe()`}.
+     * If this removes the last subscription, the connection exits Pub/Sub mode
+     * and automatic pipelining (if previously enabled) is re-enabled.
+     * See {@link subscribe `.subscribe()`} for details.

739-753: Unsubscribe docs: clarify connection-level mode exit.

Use consistent connection-level phrasing and keep notes concise.

-     * If there are no more channels subscribed to, the client automatically
-     * re-enables pipelining if it was previously enabled.
-     *
-     * Unsubscribing moves the channel back to a normal state out of the
-     * subscription state if all channels have been unsubscribed from. For
-     * further details on the subscription state, see
-     * {@link subscribe `.subscribe()`}.
+     * If this removes the last subscription, the connection exits Pub/Sub
+     * mode and automatic pipelining (if previously enabled) is re-enabled.
+     * See {@link subscribe `.subscribe()`} for details.
🧹 Nitpick comments (7)
packages/bun-types/redis.d.ts (7)

55-58: Generalize listener type; fix buffer type (Uint8Array is not generic).

Prepare for subscribeBuffer by introducing a generic listener alias, and correct the commented buffer listener type.

-    type StringPubSubListener = (message: string, channel: string) => void;
+    type SubscriptionListener<T = string> = (message: T, channel: string) => void;
+    type StringPubSubListener = SubscriptionListener<string>;
@@
-    // type BufferPubSubListener = (message: Uint8Array<ArrayBuffer>, channel: string) => void;
+    // type BufferPubSubListener = SubscriptionListener<Uint8Array>;

772-783: Unsubscribe(all): same connection-level cleanup phrasing.

-     * The client will automatically re-enable pipelining if it was previously
-     * enabled.
-     *
-     * Unsubscribing moves the channel back to a normal state out of the
-     * subscription state if all channels have been unsubscribed from. For
-     * further details on the subscription state, see
-     * {@link subscribe `.subscribe()`}.
+     * Exits Pub/Sub mode and re-enables automatic pipelining (if previously
+     * enabled). See {@link subscribe `.subscribe()`} for details.

785-799: Unsubscribe(channels[]): same connection-level cleanup phrasing.

-     * If there are no more channels subscribed to, the client automatically
-     * re-enables pipelining if it was previously enabled.
-     *
-     * Unsubscribing moves the channel back to a normal state out of the
-     * subscription state if all channels have been unsubscribed from. For
-     * further details on the subscription state, see
-     * {@link subscribe `.subscribe()`}.
+     * If this removes the last subscription, the connection exits Pub/Sub mode
+     * and re-enables automatic pipelining (if previously enabled). See
+     * {@link subscribe `.subscribe()`} for details.

800-806: Use standard JSDoc; remove non-standard @brief.

-    /**
-     * @brief Create a new RedisClient instance with the same configuration as
-     *        the current instance.
-     *
-     * This will open up a new connection to the Redis server.
-     */
+    /**
+     * Create a new RedisClient instance with the same configuration as the
+     * current instance. This opens a new connection to the Redis server.
+     */

686-696: Minor: cluster note wording.

If you want to be extra precise: in Redis Cluster, PUBLISH is node-local and does not fan out across the cluster.

-     * @returns The number of clients that received the message. Note that in a
-     * cluster this returns the total number of clients in the same node.
+     * @returns The number of clients that received the message. In Redis Cluster,
+     *          delivery is node-local; this is the number of clients on the node
+     *          that processed the publish.

697-722: Define duplicate-listener semantics explicitly.

Please document whether adding the same function more than once leads to multiple invocations or is deduped by reference. Align unsubscribe() behavior and tests with that choice.

Also applies to: 723-737


1-819: Prettier/format pass on .d.ts (as per repo guidelines).

Run Prettier to normalize JSDoc wrapping and spacing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dd2b4d1d5b3161e547580710ab8d6d34e960a62b and 44eb94420caca74bd553a8684a74bf913772d069.

📒 Files selected for processing (1)
  • packages/bun-types/redis.d.ts (16 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript files with Prettier (bun run prettier)

Files:

  • packages/bun-types/redis.d.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:58.001Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
📚 Learning: 2025-09-03T01:30:58.001Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:58.001Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • packages/bun-types/redis.d.ts
📚 Learning: 2025-09-05T20:20:04.858Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey.zig:163-178
Timestamp: 2025-09-05T20:20:04.858Z
Learning: Node-redis PUB/SUB callback error handling is broken - when a callback throws an error, it first emits the error as "Redis Client Error", then enters an undefined state with cascading internal errors like "undefined is not an object (evaluating 'this.#waitingForReply.head.value')", making the client unusable for further processing.

Applied to files:

  • packages/bun-types/redis.d.ts
🔇 Additional comments (1)
packages/bun-types/redis.d.ts (1)

469-475: Update zpopmax/zpopmin return types and JSDoc
Redis ZPOPMAX/ZPOPMIN return a [member, score] tuple (even when popping a single element), not a lone string. Change the TS signature in packages/bun-types/redis.d.ts to:

zpopmax(key: RedisClient.KeyLike): Promise<readonly [member: string, score: string] | null>;
zpopmin(key: RedisClient.KeyLike): Promise<readonly [member: string, score: string] | null>;

and adjust the JSDoc to “resolves with the removed member and its score tuple, or null if the set is empty.” Confirm against the Zig implementation’s response mapping.

Comment thread packages/bun-types/redis.d.ts Outdated
Comment thread packages/bun-types/redis.d.ts Outdated
Comment on lines 476 to 483

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.

💡 Verification agent

🧩 Analysis chain

zpopmin docs don’t match the declared return type.

Same as zpopmax; make the return description consistent with Promise<string | null> or change the type if pairs are intended.

-     * @returns Promise that resolves with the removed member and its score, or
-     * null if the set is empty
+     * @returns Promise that resolves with the removed member, or null if the set is empty

🏁 Script executed:

#!/usr/bin/env bash
# Show context for zpopmin
rg -nC3 "zpopmin" -g "packages/bun-types/redis.d.ts"
# Show context for zpopmax
rg -nC3 "zpopmax" -g "packages/bun-types/redis.d.ts"

Length of output: 991


Sync jsdoc @returns for zpopmin and zpopmax with Promise<string | null>
Change the @returns description from “resolves with the removed member and its score” to “resolves with the removed member” so it matches the Promise<string | null> signature.

🤖 Prompt for AI Agents
In packages/bun-types/redis.d.ts around lines 476-483 (and update the zpopmax
JSDoc nearby), the @returns text currently says “resolves with the removed
member and its score” but the function signature returns Promise<string | null>;
update the @returns description to “resolves with the removed member” so the doc
matches the declared return type, keeping the rest of the JSDoc unchanged.

Comment thread packages/bun-types/redis.d.ts Outdated
Comment on lines 697 to 722

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.

💡 Verification agent

❓ Verification inconclusive

Pub/Sub is connection-level; add @returns and clarify mode semantics.

Wording currently implies per-channel state. Also, the return type is Promise<number> but there’s no @returns doc.

-     * Subscribing disables automatic pipelining, so all commands will be
-     * received immediately.
-     *
-     * 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()`} are legal to invoke in a subscribed
-     * upon channel.
+     * Subscribing switches the connection into Pub/Sub mode. While in this
+     * mode, only a limited set of commands is allowed:
+     * {@link ping `.ping()`}, {@link subscribe `.subscribe()`}, and
+     * {@link unsubscribe `.unsubscribe()`}. Automatic pipelining is disabled;
+     * commands are flushed immediately.
@@
  * the channel as the second argument.
+ *
+ * @returns Promise that resolves to the total number of subscribed channels
+ *          after this call (as reported by the server).

Also, explicitly define duplicate-listener semantics (dedupe by reference vs. invoke N times). If deduping, add: “Registering the same function more than once is a no-op.” If not, add: “Registering the same function multiple times results in multiple invocations per message.”


Clarify connection‐level Pub/Sub semantics, add return docs, and define duplicate‐listener behavior

  • Replace per-channel wording with “Subscribing switches the connection into Pub/Sub mode…” and list allowed commands.
  • Add
    /**
     * @returns Promise<number> resolving to the total number of subscribed channels
     *          after this call (as reported by the server).
     */
  • Specify duplicate-listener policy (e.g. “Registering the same function more than once is a no-op.”) or, if not deduped, “results in multiple invocations per message.”
-     * Subscribing disables automatic pipelining, so all commands will be
-     * received immediately.
-     *
-     * 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()`} are legal to invoke in a subscribed
-     * upon channel.
+     * Subscribing switches the connection into Pub/Sub mode. While in this
+     * mode, only a limited set of commands is allowed:
+     * {@link ping `.ping()`}, {@link subscribe `.subscribe()`}, and
+     * {@link unsubscribe `.unsubscribe()`}. Automatic pipelining is disabled;
+     * commands are flushed immediately.
@@
  * the channel as the second argument.
+ *
+ * @returns Promise that resolves to the total number of subscribed channels
+ *          after this call (as reported by the server).
🤖 Prompt for AI Agents
In packages/bun-types/redis.d.ts around lines 697 to 722, update the subscribe
docblock to clarify connection-level Pub/Sub semantics by replacing per-channel
wording with "Subscribing switches the connection into Pub/Sub mode" and
explicitly list the only allowed commands while in that mode (e.g., .ping(),
.subscribe(), .unsubscribe()); add a @returns docblock line stating
"Promise<number> resolving to the total number of subscribed channels after this
call (as reported by the server)"; and append a sentence that specifies the
duplicate-listener policy (for example: "Registering the same listener function
more than once is a no-op." or, if the implementation invokes duplicates, state
"Registering the same listener more than once results in multiple invocations
per message.") so the behavior is unambiguous.

Comment thread packages/bun-types/redis.d.ts Outdated

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
test/js/valkey/test-utils.ts (4)

166-193: Make docker ps parsing robust; fix IPv6/dual-stack port extraction and avoid JSON parse errors

docker ps --format "{{json .}}" returns one JSON object per line; parsing the entire buffer fails when multiple matches exist. The current port parsing also breaks for IPv6 (:::49153->6379/tcp). Use line-wise JSON parsing and regex-based host port extraction.

Apply:

-    if (runningContainers.trim()) {
-      // Parse the JSON container information
-      const containerInfo = JSON.parse(runningContainers);
-      const containerName = containerInfo.Names;
-
-      // Parse port mappings from the Ports field
-      const portsString = containerInfo.Ports;
-      const portMappings = portsString.split(", ");
-
-      let port = 0;
-      let tlsPort = 0;
-
-      console.log(portMappings);
-
-      // Extract port mappings for Redis ports 6379 and 6380
-      for (const mapping of portMappings) {
-        if (mapping.includes("->6379/tcp")) {
-          const match = mapping.split("->")[0].split(":")[1];
-          if (match) {
-            port = parseInt(match);
-          }
-        } else if (mapping.includes("->6380/tcp")) {
-          const match = mapping.split("->")[0].split(":")[1];
-          if (match) {
-            tlsPort = parseInt(match);
-          }
-        }
-      }
+    if (runningContainers.trim()) {
+      // Parse possibly multiple JSON lines
+      const lines = runningContainers.split(/\r?\n/).filter(Boolean);
+      const containerInfos = lines.map(line => JSON.parse(line));
+      const containerInfo = containerInfos[0];
+      const containerName = containerInfo.Names as string;
+
+      // Robustly extract host ports; handles IPv4 and IPv6 formats
+      const portsString = String(containerInfo.Ports ?? "");
+      let port = 0;
+      let tlsPort = 0;
+      const portMatch = portsString.match(/:(\d+)->6379\/tcp/);
+      if (portMatch) port = parseInt(portMatch[1], 10);
+      const tlsMatch = portsString.match(/:(\d+)->6380\/tcp/);
+      if (tlsMatch) tlsPort = parseInt(tlsMatch[1], 10);

201-205: Fix unix socket URL scheme (missing slashes)

redis+unix: must include // to form redis+unix:///path/to.sock. Current value will not parse.

Apply:

-        UNIX_REDIS_URL = `redis+unix:${REDIS_UNIX_SOCKET}`;
+        UNIX_REDIS_URL = `redis+unix://${REDIS_UNIX_SOCKET}`;

464-465: Handle db selection for unix sockets via query, not path segments

Appending /dbId to redis+unix:///path.sock corrupts the filesystem path. Use ?db= for unix URLs; keep /dbId for tcp/tls/auth.

Apply:

-  const mkUrl = (baseUrl: string) => dbId ? `${baseUrl}/${dbId}`: baseUrl;
+  const mkUrl = (baseUrl: string) => {
+    if (!dbId) return baseUrl;
+    if (baseUrl.startsWith("redis+unix://")) {
+      const sep = baseUrl.includes("?") ? "&" : "?";
+      return `${baseUrl}${sep}db=${dbId}`;
+    }
+    return `${baseUrl}/${dbId}`;
+  };

796-799: Replace fixed sleep with readiness probe using the existing retry helper

Avoid arbitrary sleeps; poll Redis health via redis-cli ping until PONG. This makes tests resilient and aligns with the learning to avoid sleeps.

Apply:

-  // Wait a moment for the container to fully restart
-  await delay(2000);
+  // Wait for Redis to become responsive inside the container
+  await retry(async () => {
+    const ping = Bun.spawn({
+      cmd: [dockerCLI, "exec", containerName, "redis-cli", "ping"],
+      stdout: "pipe",
+      stderr: "ignore",
+      env: bunEnv,
+    });
+    const out = (await new Response(ping.stdout).text()).trim();
+    return out === "PONG";
+  }, { maxAttempts: 20, delay: 250 });

I can wire this to also respect Docker health status if you prefer using docker inspect.

♻️ Duplicate comments (17)
test/js/valkey/valkey.test.ts (7)

1-1: Remove unused sleep import to avoid flaky tests.

Since you're implementing event-driven waits (per prior learnings), the sleep import should be removed.

-import { randomUUIDv7, RedisClient, sleep } from "bun";
+import { randomUUIDv7, RedisClient } from "bun";

274-281: Replace async forEach and sleep with event-driven wait.

Using forEach(async) doesn't await publishes and sleep(flushTimeoutMs) is flaky. Use Promise.withResolvers to wait for all messages deterministically.

Add this helper at the top of the PUB/SUB describe block:

function withTimeout<T>(p: Promise<T>, ms = 3000): Promise<T> {
  let t: any;
  return Promise.race([
    p,
    new Promise<T>((_, rej) => (t = setTimeout(() => rej(new Error(`timeout after ${ms}ms`)), ms))),
  ]).finally(() => clearTimeout(t));
}

Then replace the test:

-      var receiveCount = 0;
+      let receiveCount = 0;
+      const done = Promise.withResolvers<void>();
       await subscriber.subscribe(testChannel, (message, channel) => {
         receiveCount++;
         expect(channel).toBe(testChannel);
         expect(message).toBe(testMessage);
+        if (receiveCount === TEST_MESSAGE_COUNT) done.resolve();
       });

-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        expect(await redis.publish(testChannel, testMessage)).toBe(1);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        expect(await redis.publish(testChannel, testMessage)).toBe(1);
+      }
+      await withTimeout(done.promise, 3000);

294-305: Remove all sleep() calls; use event-driven waits consistently.

Apply the same Promise.withResolvers pattern to all tests that use sleep(flushTimeoutMs) - these arbitrary delays cause flaky CI failures.

Example for "messages are received in order":

-      var receivedMessages: string[] = [];
+      const receivedMessages: string[] = [];
+      const done = Promise.withResolvers<void>();
       await subscriber.subscribe(testChannel, message => {
         receivedMessages.push(message);
+        if (receivedMessages.length === TEST_MESSAGE_COUNT) done.resolve();
       });

-      var sentMessages: string[] = [];
-      Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => {
-        const message = randomUUIDv7();
-        expect(await redis.publish(testChannel, message)).toBe(1);
-        sentMessages.push(message);
-      });
-
-      // Wait a little bit just to ensure all the messages are flushed.
-      await sleep(flushTimeoutMs);
+      const sentMessages: string[] = [];
+      for (let i = 0; i < TEST_MESSAGE_COUNT; i++) {
+        const message = randomUUIDv7();
+        expect(await redis.publish(testChannel, message)).toBe(1);
+        sentMessages.push(message);
+      }
+      await withTimeout(done.promise, 5000);

Also applies to: 327-338, 363-374, 405-406, 452-453, 491-492, 558-559, 594-603, 611-612


244-248: Use async assertion with rejects for promise-based method.

The set method returns a Promise, so use rejects matcher.

-      expect(() => redis.set(testKey, testValue)).toThrow(
-        "RedisClient.prototype.set cannot be called while in subscriber mode",
-      );
+      await expect(redis.set(testKey, testValue)).rejects.toThrow(
+        "RedisClient.prototype.set cannot be called while in subscriber mode",
+      );

485-496: Use async assertion for publish rejection.

publish returns a Promise, so use async rejection matcher instead of synchronous throw.

-      // Publishing from the same client should work
-      expect(async () => redis.publish(channel, "self-published")).toThrow();
-      await sleep(flushTimeoutMs);
+      // Publishing from a subscribed client should fail
+      await expect(redis.publish(channel, "self-published")).rejects.toThrow();

533-536: Use async rejection matcher for unsubscribe.

unsubscribe() returns a Promise.

-      expect(() => redis.unsubscribe(channel)).toThrow(
-        "RedisClient.prototype.unsubscribe can only be called while in subscriber mode.",
-      );
+      await expect(redis.unsubscribe(channel)).rejects.toThrow(
+        "RedisClient.prototype.unsubscribe can only be called while in subscriber mode.",
+      );

619-644: Close clients in duplicate() tests to avoid socket leaks.

Use try/finally blocks to ensure both original and duplicate clients are closed.

Example for first test:

       const redis = await connectedRedis();
-
-      const duplicate = await redis.duplicate();
+      try {
+        const duplicate = await redis.duplicate();
+        try {
           expect(duplicate.connected).toBe(true);
           expect(duplicate).not.toBe(redis);

           // Both should work independently
           await redis.set("test-original", "original-value");
           await duplicate.set("test-duplicate", "duplicate-value");

           expect(await redis.get("test-duplicate")).toBe("duplicate-value");
           expect(await duplicate.get("test-original")).toBe("original-value");
-
-          duplicate.close();
+        } finally {
+          duplicate.close?.();
+        }
+      } finally {
+        redis.close?.();
+      }

Also applies to: 628-645, 674-691, 710-741, 768-785

src/valkey/valkey.zig (3)

409-409: Use @TagName for enums, not @Errorname.

protocol.RedisError is an enum, not an error union.

-        debug("failed: {s}: {}", .{ message, err });
+        debug("failed: {s}: {s}", .{ message, @tagName(err) });

788-843: Defer loading in-flight promise to avoid dropping on push messages.

Loading pair_maybe upfront can drop promises when receiving unrelated push messages. Only call readItem() when you actually need to resolve a promise.

-        // Let's load the promise pair.
-        var pair_maybe = this.in_flight.readItem();
+        // Defer loading the promise pair until needed.
+        var pair_maybe: ?Command.PromisePair = null;

         // We handle subscriptions specially because they are not regular
         // commands and their failure will potentially cause the client to drop
         // out of subscriber mode.
         if (this.parent().isSubscriber()) {
             debug("This client is a subscriber. Handling as subscriber...", .{});

             // There are multiple different commands we may receive in
             // subscriber mode. One is from a client.subscribe() call which
             // requires that a promise is in-flight, but otherwise, we may also
             // receive push messages from the server that do not have an
             // associated promise.
-            if (pair_maybe) |*pair| {
+            // Check if this is a subscribe/unsubscribe response that needs a promise
+            const needs_promise = switch (value.*) {
+                .Push => |push| std.mem.eql(u8, push.kind, "subscribe") or std.mem.eql(u8, push.kind, "unsubscribe"),
+                else => true, // Non-push responses in subscriber mode need a promise
+            };
+
+            if (needs_promise) {
+                pair_maybe = this.in_flight.readItem();
+            }
+
+            if (pair_maybe) |*pair| {
                 debug("There is a request in flight. Handling as a subscribe request...", .{});
                 if (this.handleSubscribeResponse(value, pair) == .handled) {
                     return;
                 }
             }

             switch (value.*) {
                 .Error => |err| {
                     this.fail(err, protocol.RedisError.InvalidResponse);
                     return;
                 },
                 .Push => |push| {
                     if (std.mem.eql(u8, push.kind, "message")) {
                         @branchHint(.likely);
                         debug("Received a message.", .{});
                         this.onValkeyMessage(push.data);
                         return;
-                    } else if (std.mem.eql(u8, push.kind, "subscribe")) {
-                        @branchHint(.cold);
-                        debug("Received subscription message without promise: {any}", .{push.data});
-                        return;
-                    } else if (std.mem.eql(u8, push.kind, "unsubscribe")) {
-                        @branchHint(.cold);
-                        debug("Received unsubscribe message without promise: {any}", .{push.data});
-                        return;
+                    } else if (std.mem.eql(u8, push.kind, "subscribe") or std.mem.eql(u8, push.kind, "unsubscribe")) {
+                        if (pair_maybe == null) {
+                            @branchHint(.cold);
+                            debug("Received {s} message without promise: {any}", .{ push.kind, push.data });
+                        }
+                        return;
                     } else {
                         @branchHint(.cold);
                         this.fail("Unexpected push message kind without promise", protocol.RedisError.InvalidResponseType);
                         return;
                     }
                 },
                 else => {
                     // In the else case, we fall through to the regular
                     // handler. Subscribers can send .Push commands which have
                     // the same semantics as regular commands.
                 },
             }

             debug("Treating subscriber response as a regular command...", .{});
         }

         // For regular commands, get the next command+promise pair from the queue
+        if (pair_maybe == null) pair_maybe = this.in_flight.readItem();
         var pair = pair_maybe orelse {
             debug("Received response but no promise in queue", .{});
             return;
         };

659-659: Fix missing error handling for fallible toJS call.

value.toJS(globalThis) can fail and needs error handling.

-                promise_ptr.reject(globalThis, value.toJS(globalThis));
+                promise_ptr.reject(globalThis, value.toJS(globalThis) catch |inner| globalThis.takeError(inner));
src/valkey/js_valkey.zig (3)

89-90: Use JavaScript identity comparison for callbacks.

Using Zig's == operator may not correctly compare JS object identity.

-            if (iter == callback)
-                continue;
+            if (iter.isSameValue(callback))
+                continue;

124-124: Handle null like undefined for consistency.

Using isUndefined() alone makes null trigger the unreachable path.

-            if (existing_handler_arr.isUndefined()) {
+            if (existing_handler_arr.isUndefinedOrNull()) {

184-186: Don't propagate user callback errors; report them instead.

Returning the error stops processing remaining callbacks and can tear down the connection.

                 if (callback.isCallable()) {
                     _ = callback.call(globalObject, .js_undefined, args) catch |e| {
-                        return e;
+                        debug("Subscriber callback threw: {}", .{e});
+                        globalObject.reportActiveExceptionAsUnhandled(e);
                     };
                 }
             }
         } else if (callbacks.isCallable()) {
             _ = callbacks.call(globalObject, .js_undefined, args) catch |e| {
-                return e;
+                debug("Subscriber callback threw: {}", .{e});
+                globalObject.reportActiveExceptionAsUnhandled(e);
             };
         }

Also applies to: 190-192

src/valkey/js_valkey_functions.zig (4)

1117-1118: PSUBSCRIBE/PUNSUBSCRIBE bypass subscription context

These exports don’t register JS listeners or route pattern messages; users won’t receive deliveries. Either implement pattern-aware wrappers (like subscribe/unsubscribe) or gate/remove until pmessage routing exists.

#!/usr/bin/env bash
# Verify pattern message handling exists before exposing PSUBSCRIBE/PUNSUBSCRIBE
rg -nP '\bpmessage\b|\bonValkeyPMessage\b' src/valkey -C2

801-805: Always return a Promise from unsubscribe() when ctx is missing

Public API promises a Promise; returning bare undefined breaks await/then.

-    var subscription_ctx = this._subscription_ctx orelse {
-        return .js_undefined;
-    };
+    var subscription_ctx = this._subscription_ctx orelse {
+        const promise = jsc.JSPromise.create(globalObject);
+        promise.resolve(globalObject, .js_undefined);
+        return promise.toJS();
+    };

698-717: Prevent leaked subscriber state on pre-SUBSCRIBE validation errors

If per-item validation throws (e.g., non-string in the array), the subscription ctx remains and the client is “stuck” in subscriber mode. Add rollback.

     var subscription_ctx = this.getOrCreateSubscriptionCtxEnteringSubscriptionMode();
+    // Roll back if anything throws before SUBSCRIBE is sent
+    errdefer this.deleteSubscriptionCtx();

1104-1109: duplicate(): unprotect new_client_js on connection failure
In the auto-connect branch of js_valkey_functions.zig, wrap the doConnect call so that if it rejects you call new_client_js.unprotect() before returning. For example:

if (this.client.status == .connected and !this.client.flags.is_manually_closed) {
    new_client.client.flags.connection_promise_returns_client = true;
    new_client_js.protect();
    return new_client.doConnect(globalObject, new_client_js) catch |err| {
        new_client_js.unprotect();
        return err;
    };
}
🧹 Nitpick comments (10)
test/js/valkey/test-utils.ts (1)

458-462: Tighten the signature: prefer optional params over | undefined = undefined

Purely cosmetic but clearer TS.

Apply:

-export function createClient(
-  connectionType: ConnectionType = ConnectionType.TCP,
-  hostUrl: string | undefined = undefined,
-  customOptions = {},
-  dbId: number | undefined = undefined,
-) {
+export function createClient(
+  connectionType: ConnectionType = ConnectionType.TCP,
+  hostUrl?: string,
+  customOptions: Record<string, unknown> = {},
+  dbId?: number,
+) {
src/bun.js/bindings/bindings.cpp (1)

6406-6419: JSMap::get: document zero-sentinel semantics explicitly; keep behavior as-is.

The implementation correctly uses OrderedHashMap::Helper::find and returns the entry value; an empty JSValue (encoded 0) signals “not found” and distinguishes it from an existing entry with value undefined (non-zero). To prevent future confusion, tighten the comment to spell this out and note how callers detect exceptions.

Apply this comment-only tweak:

-// zero means "not found" or an exception was thrown
+// Returns the entry value.
+// - If the key is missing: returns an empty JSValue (encoded 0).
+// - If the key exists with value `undefined`: returns an encoded `undefined` (non-zero).
+// Exceptions are tracked via ThrowScope; callers should check the VM for a pending exception.

Please confirm the Zig wrapper treats a zero return as “not found” and separately checks for exceptions (e.g., via Bun__RETURN_IF_EXCEPTION) before interpreting zero as a miss.

test/js/valkey/valkey.test.ts (1)

506-508: Fix string inconsistency in error message.

The error message has inconsistent punctuation between line 247 and line 508.

-        "RedisClient.prototype.set cannot be called while in subscriber mode.",
+        "RedisClient.prototype.set cannot be called while in subscriber mode",
src/valkey/js_valkey.zig (1)

935-935: Consider using .monotonic for atomic loads.

Since pointers don't move and you're not relying on strict ordering with other memory operations, .monotonic could be sufficient here.

-        return this.has_pending_activity.load(.acquire);
+        return this.has_pending_activity.load(.monotonic);
src/valkey/js_valkey_functions.zig (6)

746-748: Typos in comment (“commoand”)

Minor docs nit.

-/// Send redis the UNSUBSCRIBE RESP command and clean up anything necessary after the unsubscribe commoand.
+/// Send redis the UNSUBSCRIBE RESP command and clean up anything necessary after the unsubscribe command.

829-833: Comment typo (“This s important”)

Small readability fix.

-        // unsubscribe from all channels.
+        // unsubscribe from all channels.
-        // This s important since this list is used to send
+        // This is important since this list is used to send

22-24: Simplify array validation in jsSend()

Checking isObject is redundant; arrays are objects. Keep it simple.

-    if (!args_array.isObject() or !args_array.isArray()) {
+    if (!args_array.isArray()) {
         return globalObject.throw("Arguments must be an array", .{});
     }

1119-1119: Guard PUBSUB with subscriber-mode check (consistent with others)

Other commands use requireNotSubscriber; PUBSUB should too.

-pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB").call;
+pub fn pubsub(
+    this: *JSValkeyClient,
+    globalObject: *jsc.JSGlobalObject,
+    callframe: *jsc.CallFrame,
+) bun.JSError!JSValue {
+    try requireNotSubscriber(this, @src().fn_name);
+    return compile.@"(...strings: string[])"("pubsub", "PUBSUB").call(this, globalObject, callframe);
+}

587-599: Use HSET under the hood; HMSET is deprecated

Keep API name if desired, but send HSET to the server.

-            .command = "HMSET",
+            .command = "HSET",

633-677: Micro: avoid heap ArrayList for two fixed args in publish()

Use a small on-stack buffer to reduce allocs.

-    var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
-    var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
-    defer {
-        for (args.items) |*item| { item.deinit(); }
-        args.deinit();
-    }
+    var args_buf: [2]JSArgument = undefined;
+    var args = args_buf[0..0];
@@
-    args.appendAssumeCapacity(channel);
+    args = args_buf[0..1];
+    args[0] = channel;
@@
-    args.appendAssumeCapacity(message);
+    args = args_buf[0..2];
+    args[1] = message;
@@
-            .args = .{ .args = args.items },
+            .args = .{ .args = args },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 44eb94420caca74bd553a8684a74bf913772d069 and 505fcba.

📒 Files selected for processing (14)
  • packages/bun-types/redis.d.ts (16 hunks)
  • src/bun.js/api/valkey.classes.ts (2 hunks)
  • src/bun.js/bindings/JSMap.zig (2 hunks)
  • src/bun.js/bindings/bindings.cpp (3 hunks)
  • src/bun.js/bindings/headers.h (3 hunks)
  • src/deps/uws/us_socket_t.zig (3 hunks)
  • src/string/StringBuilder.zig (1 hunks)
  • src/valkey/ValkeyCommand.zig (2 hunks)
  • src/valkey/js_valkey.zig (18 hunks)
  • src/valkey/js_valkey_functions.zig (20 hunks)
  • src/valkey/valkey.zig (25 hunks)
  • test/integration/bun-types/fixture/redis.ts (1 hunks)
  • test/js/valkey/test-utils.ts (4 hunks)
  • test/js/valkey/valkey.test.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/integration/bun-types/fixture/redis.ts
  • src/bun.js/api/valkey.classes.ts
  • src/deps/uws/us_socket_t.zig
  • src/bun.js/bindings/JSMap.zig
  • src/valkey/ValkeyCommand.zig
  • src/string/StringBuilder.zig
  • packages/bun-types/redis.d.ts
🧰 Additional context used
📓 Path-based instructions (15)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript files with Prettier (bun run prettier)

Files:

  • test/js/valkey/test-utils.ts
  • test/js/valkey/valkey.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/valkey/valkey.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be placed under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or use custom random port functions
In tests, use normalizeBunSnapshot when asserting snapshots
Never write tests that merely assert absence of "panic" or "uncaught exception" in output
Avoid shell commands (e.g., find, grep) in tests; use Bun.Glob and built-ins instead
Prefer snapshot tests over exact stdout equality assertions

Files:

  • test/js/valkey/valkey.test.ts
**/*.{cpp,h}

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.{cpp,h}: When exposing a JS class with public Constructor and Prototype, define three C++ types: class Foo : public JSC::DestructibleObject (if it has C++ fields), class FooPrototype : public JSC::JSNonFinalObject, and class FooConstructor : public JSC::InternalFunction
If the class has C++ data members, inherit from JSC::DestructibleObject and provide proper destruction; if it has no C++ fields (only JS properties), avoid a class and use JSC::constructEmptyObject(vm, structure) with putDirectOffset
Prefer placing the subspaceFor implementation in the .cpp file rather than the header when possible

Files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
**/*.{cpp,cc,cxx,h,hpp,hxx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format C/C++ sources/headers with clang-format (bun run clang-format)

Files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
**/*.cpp

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.cpp: Include "root.h" at the top of C++ binding files to satisfy lints
Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros
Prototype classes should subclass JSC::JSNonFinalObject, provide create/createStructure, DECLARE_INFO, finishCreation that reifies static properties, and set mayBePrototype on the Structure
Custom getters should use JSC_DEFINE_CUSTOM_GETTER, jsDynamicCast to validate this, and throwThisTypeError on mismatch
Custom setters should use JSC_DEFINE_CUSTOM_SETTER, validate this via jsDynamicCast, and store via WriteBarrier/set semantics
Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue
Constructors should subclass JSC::InternalFunction, return internalFunctionSpace in subspaceFor, set the prototype property as non-configurable/non-writable, and provide create/createStructure
Provide a setup function that builds the Prototype, Constructor, and Structure, and assigns them to the LazyClassStructure initializer
Use the cached Structure via globalObject->m_.get(globalObject) when constructing instances
Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure
Provide an extern "C" Bun____toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Files:

  • src/bun.js/bindings/bindings.cpp
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: For JS classes with a public constructor, implement Foo, FooPrototype, and FooConstructor (JSC patterns)
Define class properties using HashTableValue arrays in JSC bindings
Add iso subspaces for classes with C++ fields in JSC bindings
Cache structures in ZigGlobalObject for JSC-bound classes

Files:

  • src/bun.js/bindings/bindings.cpp
src/**/js_*.zig

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

src/**/js_*.zig: Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)
Handle reference counting correctly with ref()/deref() in JS-facing Zig code
Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
🧠 Learnings (48)
📓 Common learnings
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:58.001Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey.zig:163-178
Timestamp: 2025-09-05T20:20:04.858Z
Learning: Node-redis PUB/SUB callback error handling is broken - when a callback throws an error, it first emits the error as "Redis Client Error", then enters an undefined state with cascading internal errors like "undefined is not an object (evaluating 'this.#waitingForReply.head.value')", making the client unusable for further processing.
📚 Learning: 2025-09-03T01:30:58.001Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: test/js/valkey/valkey.test.ts:264-271
Timestamp: 2025-09-03T01:30:58.001Z
Learning: For test/js/valkey/valkey.test.ts PUB/SUB tests, avoid arbitrary sleeps and async-forEach. Instead, resolve a Promise from the subscriber callback when the expected number of messages is observed and await it with a bounded timeout (e.g., withTimeout + Promise.withResolvers) to account for Redis server→subscriber propagation.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.486Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.486Z
Learning: Applies to test/**/*.test.ts : Prefer `async/await` in tests; for a single callback, use `Promise.withResolvers()`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.486Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.486Z
Learning: Applies to test/**/*.test.ts : Do not write flaky tests: never wait for arbitrary time; wait for conditions instead

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Prefer data-driven tests (e.g., test.each) to reduce boilerplate

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-05T19:48:44.481Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/valkey.zig:0-0
Timestamp: 2025-09-05T19:48:44.481Z
Learning: In ValkeyCommand.zig, the Promise type is a wrapper that encapsulates valkey-specific behavior. The actual underlying promise is accessed via the `.promise` field, so the correct pattern is `promise_ptr.promise.resolve(...)` not `promise_ptr.resolve(...)`.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-05T20:01:37.460Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/valkey.zig:628-675
Timestamp: 2025-09-05T20:01:37.460Z
Learning: Do not suggest changing `promise_ptr.promise.resolve(...)` to `promise_ptr.resolve(...)` in ValkeyCommand.Promise contexts. The ValkeyCommand.Promise is a wrapper that requires accessing the underlying promise via the `.promise` field. The pattern `promise_ptr.promise.resolve(...)` is correct and should not be modified.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
📚 Learning: 2025-09-05T19:48:44.481Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/valkey.zig:0-0
Timestamp: 2025-09-05T19:48:44.481Z
Learning: In ValkeyCommand.Promise, when you need to resolve with already-converted JSValues (not RESPValues), use promise_ptr.promise.resolve() to bypass the RESP-to-JS conversion wrapper. The ValkeyCommand.Promise.resolve() method is for converting RESPValues, while direct .promise access is for pre-converted JSValues.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
📚 Learning: 2025-09-05T20:20:04.858Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey.zig:163-178
Timestamp: 2025-09-05T20:20:04.858Z
Learning: Node-redis PUB/SUB callback error handling is broken - when a callback throws an error, it first emits the error as "Redis Client Error", then enters an undefined state with cascading internal errors like "undefined is not an object (evaluating 'this.#waitingForReply.head.value')", making the client unusable for further processing.

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-05T18:45:29.200Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey.zig:0-0
Timestamp: 2025-09-05T18:45:29.200Z
Learning: In JSValkeyClient.cloneWithoutConnecting() in src/valkey/js_valkey.zig, the address/username/password fields must be repointed to the duplicated connection_strings buffer to avoid use-after-free when the original client is destroyed. The original client properly frees connection_strings in ValkeyClient.deinit().

Applied to files:

  • test/js/valkey/valkey.test.ts
  • src/valkey/valkey.zig
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T17:10:13.486Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.486Z
Learning: Applies to test/**/*.test.ts : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.486Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.486Z
Learning: Applies to test/**/*.test.ts : Use `using`/`await using` with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-03T17:10:13.486Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-09-03T17:10:13.486Z
Learning: Applies to test/**/*.test.ts : Do not set explicit test timeouts; rely on Bun’s built-in timeouts

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: For Node.js compatibility bugs, validate expected behavior in Node.js first, then confirm failure on Bun canary

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/{dev/*.test.ts,dev-and-prod.ts} : Explicitly assert client console logs with c.expectMessage; unasserted logs should not appear

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-08-30T00:09:39.100Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/dev-server-tests.mdc:0-0
Timestamp: 2025-08-30T00:09:39.100Z
Learning: Applies to test/bake/bake-harness.ts : Provide and maintain shared test utilities: devTest, prodTest, devAndProductionTest, Dev, Client, and helpers in the harness

Applied to files:

  • test/js/valkey/valkey.test.ts
📚 Learning: 2025-09-05T18:44:43.223Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/bun.js/bindings/bindings.cpp:6429-6432
Timestamp: 2025-09-05T18:44:43.223Z
Learning: The JSC::JSMap::size() method in JavaScriptCore returns a value that can be represented as uint32_t, and the binding functions in src/bun.js/bindings/bindings.cpp correctly use uint32_t as the return type for JSC__JSMap__size. JavaScript Maps are practically limited to 2^32 - 1 elements.

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T00:41:12.052Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-09-08T00:41:12.052Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add new V8 API mangled symbols (without leading underscore) to src/symbols.txt

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T04:44:59.101Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for classes with C++ fields in JSC bindings

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T04:44:59.101Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Define class properties using HashTableValue arrays in JSC bindings

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T00:41:12.052Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-09-08T00:41:12.052Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add new V8 API mangled symbols (with leading underscore and semicolons) to src/symbols.dyn

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T00:41:12.052Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-09-08T00:41:12.052Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Ensure all public V8 API functions are marked with BUN_EXPORT for symbol visibility

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-09-08T00:41:12.052Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-09-08T00:41:12.052Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : Add new V8 API method mangled symbols to the V8API struct in src/napi/napi.zig for both GCC/Clang and MSVC

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-09-07T14:00:36.526Z
Learnt from: RiskyMH
PR: oven-sh/bun#22258
File: src/bun.js/test/jest.zig:63-86
Timestamp: 2025-09-07T14:00:36.526Z
Learning: In Bun's Zig codebase, the established pattern for HashMap hash functions is to truncate hash values to u32, even when the underlying hash computation produces u64. This is seen throughout the codebase, such as in src/css/rules/rules.zig where u64 hashes are stored but truncated to u32 in HashMap contexts. When implementing HashContext for HashMapUnmanaged, the hash function should return u32 to match this pattern.

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Declare the extern C symbol in Zig and export a Zig-friendly alias for use

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.cpp : Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.cpp : Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros

Applied to files:

  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.cpp : Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.cpp : Provide an extern "C" Bun__<Type>__toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Applied to files:

  • src/bun.js/bindings/headers.h
📚 Learning: 2025-09-05T19:49:26.188Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey_functions.zig:852-867
Timestamp: 2025-09-05T19:49:26.188Z
Learning: In Bun’s Zig code, `.js_undefined` is a valid and preferred JSValue literal for “undefined” (e.g., resolving JSPromise). Do not refactor usages to `jsc.JSValue.jsUndefined()`, especially in src/valkey/js_valkey_functions.zig unsubscribe().

Applied to files:

  • src/valkey/valkey.zig
  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-05T19:49:26.188Z
Learnt from: markovejnovic
PR: oven-sh/bun#21728
File: src/valkey/js_valkey_functions.zig:852-867
Timestamp: 2025-09-05T19:49:26.188Z
Learning: In Bun's Zig codebase, `.js_undefined` is a valid way to represent JavaScript's undefined value when working with JSPromise.resolve() and similar JavaScript interop functions. This is the correct pattern to use rather than `jsc.JSValue.jsUndefined()`.

Applied to files:

  • src/valkey/valkey.zig
  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-03T05:09:24.272Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#22335
File: src/http.zig:1376-1393
Timestamp: 2025-09-03T05:09:24.272Z
Learning: In bun's HTTP module, `bun.http.default_allocator` is the correct allocator to use for internal HTTP buffers like MutableString initialization for response_message_buffer and compressed_body. This allocator is initialized in HTTPThread.zig as `bun.http.default_arena.allocator()` and is used consistently across the HTTP module, separate from any local `default_allocator` variables.

Applied to files:

  • src/valkey/valkey.zig
📚 Learning: 2025-09-02T17:09:17.391Z
Learnt from: taylordotfish
PR: oven-sh/bun#22227
File: src/allocators.zig:778-782
Timestamp: 2025-09-02T17:09:17.391Z
Learning: In Zig's bun codebase, the `isDefault` function in `src/allocators.zig` only needs to compare `allocator.vtable == c_allocator.vtable` rather than also checking the `ptr` field, because: (1) the codebase never creates multiple allocators that use `c_allocator.vtable` but have different `ptr`s, and (2) the default allocator vtable ignores the `ptr` field anyway, so any allocators sharing the same vtable would function identically.

Applied to files:

  • src/valkey/valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/{**/js_*.zig,bun.js/api/**/*.zig} : Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Applied to files:

  • src/valkey/valkey.zig
  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions

Applied to files:

  • src/valkey/valkey.zig
  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-09-08T04:44:59.101Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JSC-bound classes

Applied to files:

  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Handle reference counting correctly with ref()/deref() in JS-facing Zig code

Applied to files:

  • src/bun.js/bindings/bindings.cpp
  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In finalize() for objects holding JS references, release them using .deref() before destroy

Applied to files:

  • src/valkey/js_valkey.zig
  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Always implement proper cleanup in deinit() and finalize() for JS-exposed types

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to **/*.classes.ts : Use cache: true on properties that should use JSC WriteBarrier-backed caching

Applied to files:

  • src/valkey/js_valkey.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.zig)

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : For properties marked cache: true, use the generated Zig accessors (<Prop>NameSetCached/GetCached) to work with GC-owned values

Applied to files:

  • src/valkey/js_valkey_functions.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/valkey/js_valkey_functions.zig
🧬 Code graph analysis (2)
test/js/valkey/valkey.test.ts (2)
packages/bun-types/redis.d.ts (2)
  • redis (818-818)
  • RedisClient (61-807)
test/js/valkey/test-utils.ts (2)
  • DEFAULT_REDIS_URL (113-113)
  • randomCoinFlip (805-807)
src/bun.js/bindings/headers.h (1)
src/bun.js/bindings/bindings.cpp (4)
  • WebCore__AbortSignal__signal (6153-6162)
  • WebCore__AbortSignal__signal (6153-6153)
  • JSC__JSMap__get (6407-6419)
  • JSC__JSMap__get (6407-6407)
🔇 Additional comments (7)
test/js/valkey/test-utils.ts (1)

271-335: Set useUnixSocket consistently with mounts

You set useUnixSocket: true but the socket volume is commented out, so the host path won’t exist. Either enable the mount or set this flag false to avoid creating an unusable unix client in beforeAll.

Would you like me to gate unix tests on containerConfig.useUnixSocket && !isCI?

src/bun.js/bindings/headers.h (2)

193-193: Keep uint32_t return for JSMap size

This matches JSC’s practical Map limit and avoids widening the ABI unnecessarily. The binding choice is appropriate.


189-189: No remaining references to JSC__JSMap__get_; rename is fully applied.

src/bun.js/bindings/bindings.cpp (2)

40-40: Include is appropriate for OrderedHashMap::Helper::find usage.

Required header for the new JSMap::get implementation. Looks good.


6435-6438: Resolve: C++ and Zig bindings for JSC__JSMap__size are consistent

src/valkey/js_valkey.zig (2)

850-852: LGTM! Proper error handling for callback invocation.

Good job using globalObject.takeException to handle callback errors without tearing down the connection.


374-394: LGTM! Correct use-after-free fix for duplicate().

The pointer relocation logic correctly prevents UAF by adjusting pointers relative to the new buffer.

Comment thread src/valkey/js_valkey_functions.zig
@markovejnovic
markovejnovic merged commit dc3c8f7 into oven-sh:main Sep 10, 2025
55 of 57 checks passed
Jarred-Sumner added a commit that referenced this pull request Sep 10, 2025
@markovejnovic markovejnovic mentioned this pull request Sep 11, 2025
4 of 6 tasks
Jarred-Sumner added a commit that referenced this pull request Sep 26, 2025
### What does this PR do?

**This PR is created because [the previous PR I
opened](#21728) had some concerning
issues.** Thanks @Jarred-Sumner for the help.

The goal of this PR is to introduce PUB/SUB functionality to the
built-in Redis client. Based on the fact that the current Redis API does
not appear to have compatibility with `io-redis` or `redis-node`, I've
decided to do away with existing APIs and API compatibility with these
existing libraries.

I have decided to base my implementation on the [`redis-node` pub/sub
API](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md).

#### Random Things That Happened

- [x] Refactored the build scripts so that `valgrind` can be disabled.
- [x] Added a `numeric` namespace in `harness.ts` with useful
mathematical libraries.
- [x] Added a mechanism in `cppbind.ts` to disable static assertions
(specifically to allow `check_slow` even when returning a `JSValue`).
Implemented via `// NOLINT[NEXTLINE]?\(.*\)` macros.
- [x] Fixed inconsistencies in error handling of `JSMap`.

### How did you verify your code works?

I've written a set of unit tests to hopefully catch the major use-cases
of this feature. They all appear to pass.


#### Future Improvements

I would have a lot more confidence in our Redis implementation if we
tested it with a test suite running over a network which emulates a high
network failure rate. There are large amounts of edge cases that are
worthwhile to grab, but I think we can roll that out in a future PR.

### Future Tasks

- [ ] Tests over flaky network
- [ ] Use the custom private members over `_<member>`.

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
liooil pushed a commit to liooil/poly that referenced this pull request Aug 7, 2026
### What does this PR do?

**This PR is created because [the previous PR I
opened](oven-sh/bun#21728) had some concerning
issues.** Thanks @Jarred-Sumner for the help.

The goal of this PR is to introduce PUB/SUB functionality to the
built-in Redis client. Based on the fact that the current Redis API does
not appear to have compatibility with `io-redis` or `redis-node`, I've
decided to do away with existing APIs and API compatibility with these
existing libraries.

I have decided to base my implementation on the [`redis-node` pub/sub
API](https://github.com/redis/node-redis/blob/master/docs/pub-sub.md).

#### Random Things That Happened

- [x] Refactored the build scripts so that `valgrind` can be disabled.
- [x] Added a `numeric` namespace in `harness.ts` with useful
mathematical libraries.
- [x] Added a mechanism in `cppbind.ts` to disable static assertions
(specifically to allow `check_slow` even when returning a `JSValue`).
Implemented via `// NOLINT[NEXTLINE]?\(.*\)` macros.
- [x] Fixed inconsistencies in error handling of `JSMap`.

### How did you verify your code works?

I've written a set of unit tests to hopefully catch the major use-cases
of this feature. They all appear to pass.


#### Future Improvements

I would have a lot more confidence in our Redis implementation if we
tested it with a test suite running over a network which emulates a high
network failure rate. There are large amounts of edge cases that are
worthwhile to grab, but I think we can roll that out in a future PR.

### Future Tasks

- [ ] Tests over flaky network
- [ ] Use the custom private members over `_<member>`.

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
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.

5 participants