Repository navigation
Redis PUB/SUB - #21728
Redis PUB/SUB#21728
Conversation
|
Really interested in seeing this move forward |
WalkthroughAdds 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
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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.
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 detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
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;.zerowon’t detect “missing” and conflates undefined.JSMap.get_ returns
undefinedwhen key is absent. Comparing to.zerowon’t catch that. Returnnullonly when key is absent (has==false); otherwise return the actual value (includingundefined).- 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 exposingsubscriptionCtxas a public value.If this is internal state, omit it from
values. If it should be public and cached, prefer a getter withcache: trueper 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_tin 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. Preferrejects.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/punsubscribeto 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 thaton("message")uses (channel, message) arg order.Docs are fine here; ensure it explicitly contrasts with
subscribelistener 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 athen/catchlike insubscribe(). 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
channelOrManytwice (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.cppsrc/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.cppsrc/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.cppsrc/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.zigsrc/bun.js/bindings/JSMap.zigsrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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 invokinglog("...", .{})
Files:
src/deps/uws/us_socket_t.zigsrc/bun.js/bindings/JSMap.zigsrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.tssrc/bun.js/api/valkey.classes.tspackages/bun-types/redis.d.tstest/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.tstest/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.tstest/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.tstest/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.zigsrc/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.zigsrc/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.cppsrc/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.zigsrc/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
u32in 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 inheaders.h(line 193), single definition inbindings.cpp(line 6419), and Zig binding inJSMap.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
thenis attached (see above), they’ll correctly manage cleanup.
There was a problem hiding this comment.
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 andfail()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/valkeyAlso 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
messagetostringto 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, calldeleteSubscriptionCtx(); 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.zigAlso 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 finalunsubscribepush 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.zigsrc/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 invokinglog("...", .{})
Files:
src/valkey/js_valkey_functions.zigsrc/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 earlyreturn, 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
|
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:
We hope to see this feature released with EDIT: Drop |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (6)
src/valkey/valkey.zig (1)
621-665: Possible critical issue with subscriber response handlingThe
handleSubscribeResponsefunction 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 localhostHardcoding 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 waitingUsing
forEach(async ...)doesn't await the promises, andsleep(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 functionThe 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 handlingThe 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 implementationThe
psubscribeandpunsubscribefunctions are exported but there's no correspondingpmessagehandler 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 testsTest 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.zigsrc/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 invokinglog("...", .{})
Files:
src/valkey/js_valkey_functions.zigsrc/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.tssrc/valkey/js_valkey_functions.zigsrc/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 loggingThe new
debugmethod provides clear client identification in logs, making it much easier to debug multi-client scenarios.
1054-1056: Sensible const/non-const accessor patternThe addition of
cparent()alongsideparent()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 hintsThe 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 implementationThe 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 restrictionsThe test correctly validates that regular commands are blocked in subscriber mode with appropriate error messages.
276-300: Excellent message ordering verification testUsing 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 addedExcellent work adding subscriber mode checks to all non-subscription commands. This prevents protocol violations and provides clear error messages.
763-855: Well-structured unsubscribe implementationThe unsubscribe function properly handles both full unsubscription (no args) and selective channel unsubscription, correctly managing the subscription context lifecycle.
There was a problem hiding this comment.
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 -C2Also 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.zigsrc/valkey/valkey.zigsrc/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 invokinglog("...", .{})
Files:
src/valkey/js_valkey.zigsrc/valkey/valkey.zigsrc/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.zigsrc/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.zigsrc/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.tssrc/valkey/valkey.zigsrc/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.zigsrc/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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 betest/js/valkey/valkey.test.ts (3)
215-219: Use test harness URL instead of hardcoded localhost.Hardcoding
redis://localhost:6379will fail on CI. UseDEFAULT_REDIS_URLfrom 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: AvoidforEach(async)and arbitrary sleeps; wait for subscriber conditions.Using
forEach(async ...)doesn't await the promises, andsleep(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
afterEachhook 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@tagNamefor enum, not@errorName.
protocol.RedisErroris an enum type, not an error union, so@errorNameis 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.tstest/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.zigsrc/valkey/valkey.zigsrc/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 invokinglog("...", .{})
Files:
src/valkey/js_valkey_functions.zigsrc/valkey/valkey.zigsrc/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.zigsrc/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.zigsrc/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.tstest/js/valkey/valkey.test.tssrc/valkey/js_valkey_functions.zigsrc/valkey/valkey.zigsrc/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.zigsrc/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
There was a problem hiding this comment.
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.zigsrc/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.zigsrc/valkey/js_valkey_functions.zigsrc/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 invokinglog("...", .{})
Files:
src/valkey/valkey.zigsrc/valkey/js_valkey_functions.zigsrc/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.zigsrc/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.zigsrc/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.zigsrc/valkey/js_valkey_functions.zigsrc/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.zigsrc/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.
|
Need to figure out what |
There was a problem hiding this comment.
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 handledAlso 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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/js_valkey.zigsrc/valkey/valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/js_valkey.zigsrc/valkey/valkey.zigsrc/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.zigsrc/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.zigsrc/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.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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.zigtest/js/valkey/valkey.test.tssrc/valkey/valkey.zigsrc/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.zigsrc/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 — LGTMConsistent, 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 semanticsYou 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.
There was a problem hiding this comment.
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 underflowThere’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 everyconnectedRedis()invocation in atry/finally, unsubscribe if subscribed, then callclient.close(). For example, in test/js/valkey/valkey.test.ts around the tests beginning at lines 221, 232, 252, etc., ensure eachredisandsubscriberis closed to prevent resource leaks.src/valkey/js_valkey.zig (4)
850-868: Double deref in finalize() — remove the explicit callYou 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 arraysUse 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 throwsCatching 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 handledThe 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 ALLWhen 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 ValkeyPrefer consistent naming in logs.
- debug("Connecting to Redis.", .{}); + debug("Connecting to Valkey.", .{});
28-30: Remove stale TODO; implementation looks correctsubscriptionCount() 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 → ValkeyJSMinor 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 updatesPrefer 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.tstest/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.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.zigsrc/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.zigsrc/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.tstest/js/valkey/valkey.test.tssrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.
There was a problem hiding this comment.
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_ptris already*Command.Promise. Dereferencing.promisewill 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.Pushpackets; 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 failureIf the returned Promise rejects later, the client remains “stuck” in subscriber mode. Attach a
.thenwith a rejection callback that callsdeleteSubscriptionCtx().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
arg1as 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 handlingIf pattern messages aren’t fully wired (no
pmessagedispatch), exposing these is misleading. Either:
- implement
pmessageand route to a handler, or- feature-gate/remove these exports for now.
I can wire
pmessageinto 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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/valkey.zigsrc/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.zigsrc/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 semanticsThe
connection_promise_returns_clientflag is clear and scoped; docs explain both code paths well.
1058-1061: Const-parent accessor LGTMUsing a const variant avoids unnecessary casts and clarifies readonly access.
1070-1080: Clean public-API bridging for subscribe/unsubscribe/messageForwarders 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 — niceBlocking 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
There was a problem hiding this comment.
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.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/js_valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/js_valkey.zigsrc/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.zigsrc/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.zigsrc/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.tssrc/valkey/js_valkey.zigsrc/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().
There was a problem hiding this comment.
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.
toThrowErrorMatchingInlineSnapshotexpects 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 andsleep(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.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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 invokinglog("...", .{})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.JSValueRun 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.tssrc/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
There was a problem hiding this comment.
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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/js_valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/js_valkey.zigsrc/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.zigsrc/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.zigsrc/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.zigsrc/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.
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`
|
Note Generated docstrings for this pull request at #22411 |
There was a problem hiding this comment.
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 clientsUse
DEFAULT_REDIS_URLto 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-subscriberPublishing 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 matcherWithout 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 conditionUse 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 sleepsSwitch 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 insteadReplace 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 rulePublish 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 asynchronouslyUse
.rejectswith 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);andlog("...", .{}). Consider renamingdebug→logand usingfalseinstead of.visibleto 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 slicesYou 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 optionBreaking 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; theisObject()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
cis 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.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/deps/uws/us_socket_t.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/deps/uws/us_socket_t.zigsrc/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.tssrc/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: LGTMSwitching 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 coverageAdding 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.
There was a problem hiding this comment.
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_URLis set toredis+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 namedvalkey-unified-test-bun-..., butgetRedisContainerName()filtersdocker psbyname=valkey-unified-test, sorestartRedisContainer()throws “No Redis container found.”- Prefer the already-computed
containerConfig.containerNameif available, and fall back todocker psusing thevalkey-unified-test-bunprefix.- 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?.connectedis 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.resolvesso 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_URLfrom 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: AvoidforEach(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, notsleep.- 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.resolveon a*Promise.
promise_ptris already a*Command.Promise; invokingpromise_ptr.promise.resolvewon’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/punsubscribeare exported but patternpmessagedelivery isn’t shown. Either implementpmessagerouting 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, updatepublishto 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: Parsingdocker ps --format {{json .}}assumes a single line; make it robust.If multiple containers match,
runningContainershas multiple JSON lines, andJSON.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.5and===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 properundefinedand 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.tstest/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.tstest/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.tstest/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.tstest/js/valkey/test-utils.ts
test/**/*.test.ts
📄 CodeRabbit inference engine (test/CLAUDE.md)
test/**/*.test.ts: Name test files*.test.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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.tstest/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 invokinglog("...", .{})In Zig code, manage memory carefully: use appropriate allocators and defer for cleanup
Files:
src/valkey/valkey.zigsrc/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.JSValueRun zig-format to format Zig files (bun run zig-format)
Files:
src/valkey/valkey.zigsrc/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.tssrc/valkey/valkey.zigsrc/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)
There was a problem hiding this comment.
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. UseJSArgumentfor 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()eachJSArgument(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 callsthis.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 sendAlso applies to: 859-862
1206-1230: duplicate(): minor safety — ensure unprotect on early error.If
doConnectthrows synchronously,new_client_jsremains 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 invokinglog("...", .{})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.JSValueRun 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
JSArgumentin your HMSET/HMGET loops to handle buffers correctly.- Manually verify your Redis client’s
.d.tsdeclarations forhmset/hset/hmgetmatch the new varargs or object signature and support binary inputs.
There was a problem hiding this comment.
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
Uint8Arrayhas 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.
There was a problem hiding this comment.
💡 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.
There was a problem hiding this comment.
💡 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.
There was a problem hiding this comment.
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: Makedocker psparsing 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 formredis+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 segmentsAppending
/dbIdtoredis+unix:///path.sockcorrupts the filesystem path. Use?db=for unix URLs; keep/dbIdfor 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 helperAvoid arbitrary sleeps; poll Redis health via
redis-cli pinguntil 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
sleepimport 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 andsleep(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
setmethod returns a Promise, so userejectsmatcher.- 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.
publishreturns 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.RedisErroris 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_maybeupfront can drop promises when receiving unrelated push messages. Only callreadItem()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 contextThese 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 missingPublic 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 errorsIf 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 thedoConnectcall so that if it rejects you callnew_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 = undefinedPurely 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,
.monotoniccould 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 deprecatedKeep 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.tstest/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.tstest/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.tstest/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.tstest/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.tstest/js/valkey/valkey.test.ts
test/**/*.test.ts
📄 CodeRabbit inference engine (test/CLAUDE.md)
test/**/*.test.ts: Name test files*.test.tsand usebun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; useport: 0to get a random port
When spawning Bun in tests, usebunExe()andbunEnvfromharness
Preferasync/awaitin tests; for a single callback, usePromise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
UsetempDir/tempDirWithFilesfromharnessfor temporary files and directories in tests
For large/repetitive strings in tests, preferBuffer.alloc(count, fill).toString()over"A".repeat(count)
Import common test utilities fromharness(e.g.,bunExe,bunEnv,tempDirWithFiles,tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and usetoThrowfor synchronous errors
Usedescribeblocks for grouping,describe.eachfor parameterized tests, snapshots withtoMatchSnapshot, and lifecycle hooks (beforeAll,beforeEach,afterEach); track resources for cleanup inafterEach
Useusing/await usingwith 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.hsrc/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.hsrc/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 invokinglog("...", .{})
Files:
src/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.zigsrc/valkey/js_valkey.zigsrc/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.zigsrc/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.zigsrc/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.tssrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.tssrc/valkey/valkey.zigsrc/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.tssrc/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.tssrc/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.tssrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.tssrc/valkey/valkey.zigsrc/valkey/js_valkey.zigsrc/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.hsrc/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.hsrc/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.hsrc/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.hsrc/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.hsrc/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.hsrc/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.hsrc/bun.js/bindings/bindings.cppsrc/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.hsrc/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.zigsrc/bun.js/bindings/bindings.cppsrc/valkey/js_valkey.zigsrc/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.zigsrc/bun.js/bindings/bindings.cppsrc/valkey/js_valkey.zigsrc/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.zigsrc/bun.js/bindings/bindings.cppsrc/valkey/js_valkey.zigsrc/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.zigsrc/bun.js/bindings/bindings.cppsrc/valkey/js_valkey.zigsrc/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.cppsrc/valkey/js_valkey.zigsrc/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.zigsrc/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: SetuseUnixSocketconsistently with mountsYou set
useUnixSocket: truebut 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 inbeforeAll.Would you like me to gate unix tests on
containerConfig.useUnixSocket && !isCI?src/bun.js/bindings/headers.h (2)
193-193: Keepuint32_treturn for JSMap sizeThis matches JSC’s practical Map limit and avoids widening the ABI unnecessarily. The binding choice is appropriate.
189-189: No remaining references toJSC__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 consistentsrc/valkey/js_valkey.zig (2)
850-852: LGTM! Proper error handling for callback invocation.Good job using
globalObject.takeExceptionto 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.
This reverts commit dc3c8f7.
### 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>
### 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>
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-redisorredis-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-nodepub/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:
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
_<member>.