Summary
Under perry compile, calling conn.command('SET', 'k', 'v') on a @perryts/redis Connection instance reaches the encoder with args.length === 1 (instead of 2), and the single element is a denormal-double (same NaN-box symptom as #653, which fixed the spread-side variant). The bytes sent to Redis are then SET <denormal> → -ERR wrong number of arguments for 'set' command.
Passing the args as an explicit array via commandArray(name, args) — same sendCommand callee, just no rest-collection — works correctly.
I could not minimize the repro outside the driver. Isolated tests with rest + generic + class + private-field + union-type + cross-module imports all collect args correctly. There's something specific to the actual @perryts/redis Connection class's shape that hits this path. Filing anyway because the symptom is reliable in-driver and the workaround indirection shouldn't be necessary.
Environment
- perry 0.5.795
- macOS 26.4 / Apple M1 Max
Reliable in-driver repro
cd /tmp && bun add @perryts/redis
# (or: git clone https://github.com/PerryTS/redis.git; cd redis; bun install)
// repro.ts
import { connect } from '@perryts/redis';
async function main() {
const conn = await connect({ host: '127.0.0.1', port: 6379, protocol: 2 });
// Rest form — fails under AOT, works on Node + Bun.
try {
const r = await conn.command<string>('SET', 'k', 'v');
console.log('command(rest): ', r.value);
} catch (e: any) { console.log('command(rest) ERR:', e.message); }
// Explicit-array form — works everywhere.
const r2 = await conn.commandArray<string>('SET', ['k', 'v']);
console.log('commandArray: ', r2.value);
await conn.close();
process.exit(0);
}
main();
$ redis-server --port 6379 --daemonize yes --save '' --appendonly no
$ perry compile repro.ts -o repro && ./repro
connected
command(rest) ERR: ERR wrong number of arguments for 'set' command
commandArray: OK
Node + Bun output (correct on both call shapes):
connected
command(rest): OK
commandArray: OK
What Connection.command() looks like
class Connection {
private readonly id: number;
// ...
command<T = unknown>(name: string, ...args: CommandArg[]): Promise<CommandReply<T>> {
return sendCommand<T>(this.id, name, args);
}
commandArray<T = unknown>(name: string, args: CommandArg[]): Promise<CommandReply<T>> {
return sendCommand<T>(this.id, name, args);
}
}
type CommandArg = string | Buffer | number | bigint;
The two methods differ only by ...args vs args. Same callee, same Promise shape. The rest form drops args under AOT; the array form doesn't.
Things I tried that did NOT reproduce
- Generic async method with rest + union type + class + cross-module import (worked).
- Same with a private numeric field on the class (worked).
- Multi-level pass-through of the rest-collected array through three function calls (worked).
- Class method with
Map<number, State> lookup using the id field (worked).
So the trigger is something cumulative in the actual Connection class — possibly the number of other methods, interaction with the imports at the top of src/connection.ts (it imports from ./protocol/reader, ./protocol/writer, ./protocol/types, ./transport/net-socket, etc.), or interaction with the side-effect imports (./register-defaults, ./auth/handshake-hook). I'd be happy to bisect if you can suggest where to look.
Related
Workaround in @perryts/redis
Added commandArray(name, args) and pipeline.addArray(name, args) companions to the rest-form methods. Bun and Node ignore the indirection; AOT users have to call the array form until this is fixed.
Summary
Under
perry compile, callingconn.command('SET', 'k', 'v')on a@perryts/redisConnectioninstance reaches the encoder withargs.length === 1(instead of2), and the single element is a denormal-double (same NaN-box symptom as #653, which fixed the spread-side variant). The bytes sent to Redis are thenSET <denormal>→-ERR wrong number of arguments for 'set' command.Passing the args as an explicit array via
commandArray(name, args)— samesendCommandcallee, just no rest-collection — works correctly.I could not minimize the repro outside the driver. Isolated tests with rest + generic + class + private-field + union-type + cross-module imports all collect args correctly. There's something specific to the actual
@perryts/redisConnection class's shape that hits this path. Filing anyway because the symptom is reliable in-driver and the workaround indirection shouldn't be necessary.Environment
Reliable in-driver repro
Node + Bun output (correct on both call shapes):
What
Connection.command()looks likeThe two methods differ only by
...argsvsargs. Same callee, same Promise shape. The rest form drops args under AOT; the array form doesn't.Things I tried that did NOT reproduce
Map<number, State>lookup using the id field (worked).So the trigger is something cumulative in the actual Connection class — possibly the number of other methods, interaction with the imports at the top of
src/connection.ts(it imports from./protocol/reader,./protocol/writer,./protocol/types,./transport/net-socket, etc.), or interaction with the side-effect imports (./register-defaults,./auth/handshake-hook). I'd be happy to bisect if you can suggest where to look.Related
Workaround in
@perryts/redisAdded
commandArray(name, args)andpipeline.addArray(name, args)companions to the rest-form methods. Bun and Node ignore the indirection; AOT users have to call the array form until this is fixed.