Skip to content

Commit

Permalink
feat: 🎸 support UTF8 encoding in command arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Dec 17, 2023
1 parent d030a02 commit 76deb51
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {RedisCluster} from '../../../cluster/RedisCluster';
import {ClusterTestSetup} from '../../types';
import {run} from './SET';
import {RedisCluster} from '../cluster/RedisCluster';
import {ClusterTestSetup} from './types';
import * as commands from './commands';

const host = 'redis-15083.c28691.us-east-1-4.ec2.cloud.rlrcp.com';
const port = 15083;
Expand All @@ -23,7 +23,9 @@ const setupCluster: ClusterTestSetup = async () => {
return {client};
};

run(setupCluster);
describe('cluster', () => {
commands.run(setupCluster);
});

afterAll(() => {
client.stop();
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {TestSetup} from '../types';
import * as string from './string';

export const run = (setup: TestSetup) => {
describe('string commands', () => {
string.run(setup);
});
};
36 changes: 36 additions & 0 deletions src/__tests__/commands/string/GET.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {TestSetup} from '../../types';

export const run = (setup: TestSetup) => {
describe('GET', () => {
test('missing key returns null', async () => {
const {client} = await setup();
const res = await client.cmd(['GET', 'missing_key']);
expect(res).toBe(null);
});

test('can get a key', async () => {
const {client} = await setup();
const key = 'fetch_existing_key_' + Date.now();
await client.cmd(['SET', key, '42']);
const res = await client.cmd(['GET', key], {utf8Res: true});
expect(res).toBe('42');
});

test('key can contain UTF-8 characters', async () => {
const {client} = await setup();
const key = 'key_with_emoji_😛_' + Date.now();
await client.cmd(['SET', key, '42'], {utf8: true});
const keyBuf = Buffer.from(key);
const res = await client.cmd(['GET', keyBuf], {utf8Res: true});
expect(res).toBe('42');
});

test('value can contain UTF-8 characters', async () => {
const {client} = await setup();
const key = 'value_with_emoji_' + Date.now();
await client.cmd(['SET', key, '😅'], {utf8: true});
const res = await client.cmd(['GET', key], {utf8Res: true});
expect(res).toBe('😅');
});
});
};
4 changes: 2 additions & 2 deletions src/__tests__/commands/string/SET.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ClusterTestSetup} from '../../types';
import {TestSetup} from '../../types';

export const run = (setup: ClusterTestSetup) => {
export const run = (setup: TestSetup) => {
describe('SET', () => {
test('can set a key', async () => {
const {client} = await setup();
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/commands/string/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {TestSetup} from '../../types';
import * as SET from './SET';
import * as GET from './GET';

export const run = (setup: TestSetup) => {
describe('string commands', () => {
SET.run(setup);
GET.run(setup);
});
};
1 change: 1 addition & 0 deletions src/cluster/RedisCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export class RedisCluster implements Printable {
public async cmd(args: Cmd | MultiCmd, opts?: ClusterCmdOpts): Promise<unknown> {
const call = new RedisClusterCall(args);
if (opts) {
if (opts.utf8) call.utf8 = true;
if (opts.utf8Res) call.utf8Res = true;
if (opts.noRes) call.noRes = true;
if (opts.key) call.key = opts.key;
Expand Down
5 changes: 5 additions & 0 deletions src/node/RedisCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const callNoRes = (args: Cmd | MultiCmd) => {
* Represents a single Redis request/response command call.
*/
export class RedisCall {
/**
* Whether to encode command arguments as UTF-8 strings.
*/
public utf8: boolean = false;

/**
* Whether to try to decode RESP responses binary strings as UTF-8 strings.
*/
Expand Down
17 changes: 14 additions & 3 deletions src/node/RedisClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ export class RedisClient {
});
}


// ------------------------------------------------------------------- Events

public readonly onProtocolError = new Defer<Error>();


// ------------------------------------------------------------ Socket writes

protected readonly encoder: RespEncoder;
Expand All @@ -51,8 +53,12 @@ export class RedisClient {
const cmd = call.args;
if (isMultiCmd(cmd)) {
const length = cmd.length;
for (let i = 0; i < length; i++) encoder.writeCmd(cmd[i]);
} else encoder.writeCmd(cmd);
if (call.utf8) for (let i = 0; i < length; i++) encoder.writeCmdUtf8(cmd[i]);
else for (let i = 0; i < length; i++) encoder.writeCmd(cmd[i]);
} else {
if (call.utf8) encoder.writeCmdUtf8(cmd);
else encoder.writeCmd(cmd);
}
}
const buf = encoder.writer.flush();
// console.log(Buffer.from(buf).toString());
Expand All @@ -64,6 +70,7 @@ export class RedisClient {
}
};


// ------------------------------------------------------------- Socket reads

protected readonly decoder: RespStreamingDecoder;
Expand Down Expand Up @@ -104,6 +111,7 @@ export class RedisClient {
}
};


// -------------------------------------------------------------- Life cycles

public start() {
Expand All @@ -114,6 +122,7 @@ export class RedisClient {
this.socket.stop();
}


// -------------------------------------------------------- Command execution

public async call(call: RedisCall): Promise<unknown> {
Expand All @@ -127,6 +136,7 @@ export class RedisClient {
public async cmd(args: Cmd | MultiCmd, opts?: CmdOpts): Promise<unknown> {
const call = new RedisCall(args);
if (opts) {
if (opts.utf8) call.utf8 = true;
if (opts.utf8Res) call.utf8Res = true;
if (opts.noRes) call.noRes = true;
}
Expand All @@ -143,6 +153,7 @@ export class RedisClient {
this.callFnf(callNoRes(args));
}


// -------------------------------------------------------- Built-in commands

/** Authenticate and negotiate protocol version. */
Expand All @@ -161,4 +172,4 @@ export class RedisClient {
}
}

export type CmdOpts = Partial<Pick<RedisCall, 'utf8Res' | 'noRes'>>;
export type CmdOpts = Partial<Pick<RedisCall, 'utf8' | 'utf8Res' | 'noRes'>>;

0 comments on commit 76deb51

Please sign in to comment.