Skip to content

Commit 2711c88

Browse files
committed
Add client side caching RESP3 validation
1 parent a4403c9 commit 2711c88

File tree

6 files changed

+206
-57
lines changed

6 files changed

+206
-57
lines changed

packages/client/lib/client/index.spec.ts

+49-15
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,44 @@ export const SQUARE_SCRIPT = defineScript({
2424
});
2525

2626
describe('Client', () => {
27+
describe('initialization', () => {
28+
describe('clientSideCache validation', () => {
29+
const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
30+
31+
it('should throw error when clientSideCache is enabled with RESP 2', () => {
32+
assert.throws(
33+
() => new RedisClient({
34+
clientSideCache: clientSideCacheConfig,
35+
RESP: 2,
36+
}),
37+
new Error('Client Side Caching is only supported with RESP3')
38+
);
39+
});
40+
41+
it('should throw error when clientSideCache is enabled with RESP undefined', () => {
42+
assert.throws(
43+
() => new RedisClient({
44+
clientSideCache: clientSideCacheConfig,
45+
}),
46+
new Error('Client Side Caching is only supported with RESP3')
47+
);
48+
});
49+
50+
it('should not throw when clientSideCache is enabled with RESP 3', () => {
51+
assert.doesNotThrow(() =>
52+
new RedisClient({
53+
clientSideCache: clientSideCacheConfig,
54+
RESP: 3,
55+
})
56+
);
57+
});
58+
});
59+
});
60+
2761
describe('parseURL', () => {
2862
it('redis://user:secret@localhost:6379/0', async () => {
2963
const result = RedisClient.parseURL('redis://user:secret@localhost:6379/0');
30-
const expected : RedisClientOptions = {
64+
const expected: RedisClientOptions = {
3165
socket: {
3266
host: 'localhost',
3367
port: 6379
@@ -51,8 +85,8 @@ describe('Client', () => {
5185
// Compare non-function properties
5286
assert.deepEqual(resultRest, expectedRest);
5387

54-
if(result.credentialsProvider.type === 'async-credentials-provider'
55-
&& expected.credentialsProvider.type === 'async-credentials-provider') {
88+
if (result?.credentialsProvider?.type === 'async-credentials-provider'
89+
&& expected?.credentialsProvider?.type === 'async-credentials-provider') {
5690

5791
// Compare the actual output of the credentials functions
5892
const resultCreds = await result.credentialsProvider.credentials();
@@ -91,10 +125,10 @@ describe('Client', () => {
91125

92126
// Compare non-function properties
93127
assert.deepEqual(resultRest, expectedRest);
94-
assert.equal(resultCredProvider.type, expectedCredProvider.type);
128+
assert.equal(resultCredProvider?.type, expectedCredProvider?.type);
95129

96-
if (result.credentialsProvider.type === 'async-credentials-provider' &&
97-
expected.credentialsProvider.type === 'async-credentials-provider') {
130+
if (result?.credentialsProvider?.type === 'async-credentials-provider' &&
131+
expected?.credentialsProvider?.type === 'async-credentials-provider') {
98132

99133
// Compare the actual output of the credentials functions
100134
const resultCreds = await result.credentialsProvider.credentials();
@@ -150,11 +184,11 @@ describe('Client', () => {
150184

151185
testUtils.testWithClient('Client can authenticate using the streaming credentials provider for initial token acquisition',
152186
async client => {
153-
assert.equal(
154-
await client.ping(),
155-
'PONG'
156-
);
157-
}, GLOBAL.SERVERS.STREAMING_AUTH);
187+
assert.equal(
188+
await client.ping(),
189+
'PONG'
190+
);
191+
}, GLOBAL.SERVERS.STREAMING_AUTH);
158192

159193
testUtils.testWithClient('should execute AUTH before SELECT', async client => {
160194
assert.equal(
@@ -408,7 +442,7 @@ describe('Client', () => {
408442
});
409443

410444
testUtils.testWithClient('functions', async client => {
411-
const [,, reply] = await Promise.all([
445+
const [, , reply] = await Promise.all([
412446
loadMathFunction(client),
413447
client.set('key', '2'),
414448
client.math.square('key')
@@ -522,8 +556,8 @@ describe('Client', () => {
522556
const hash: Record<string, string> = {};
523557
const expectedFields: Array<string> = [];
524558
for (let i = 0; i < 100; i++) {
525-
hash[i.toString()] = i.toString();
526-
expectedFields.push(i.toString());
559+
hash[i.toString()] = i.toString();
560+
expectedFields.push(i.toString());
527561
}
528562

529563
await client.hSet('key', hash);
@@ -842,7 +876,7 @@ describe('Client', () => {
842876

843877
testUtils.testWithClient('should be able to go back to "normal mode"', async client => {
844878
await Promise.all([
845-
client.monitor(() => {}),
879+
client.monitor(() => { }),
846880
client.reset()
847881
]);
848882
await assert.doesNotReject(client.ping());

packages/client/lib/client/index.ts

+48-42
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ export interface RedisClientOptions<
7979
pingInterval?: number;
8080
/**
8181
* Default command options to be applied to all commands executed through this client.
82-
*
82+
*
8383
* These options can be overridden on a per-command basis when calling specific commands.
84-
*
84+
*
8585
* @property {symbol} [chainId] - Identifier for chaining commands together
8686
* @property {boolean} [asap] - When true, the command is executed as soon as possible
8787
* @property {AbortSignal} [abortSignal] - AbortSignal to cancel the command
8888
* @property {TypeMapping} [typeMapping] - Custom type mappings between RESP and JavaScript types
89-
*
89+
*
9090
* @example Setting default command options
9191
* ```
9292
* const client = createClient({
@@ -102,33 +102,33 @@ export interface RedisClientOptions<
102102
commandOptions?: CommandOptions<TYPE_MAPPING>;
103103
/**
104104
* Client Side Caching configuration.
105-
*
106-
* Enables Redis Servers and Clients to work together to cache results from commands
105+
*
106+
* Enables Redis Servers and Clients to work together to cache results from commands
107107
* sent to a server. The server will notify the client when cached results are no longer valid.
108-
*
108+
*
109109
* Note: Client Side Caching is only supported with RESP3.
110-
*
110+
*
111111
* @example Anonymous cache configuration
112112
* ```
113113
* const client = createClient({
114-
* RESP: 3,
114+
* RESP: 3,
115115
* clientSideCache: {
116116
* ttl: 0,
117117
* maxEntries: 0,
118-
* evictPolicy: "LRU"
118+
* evictPolicy: "LRU"
119119
* }
120120
* });
121121
* ```
122-
*
122+
*
123123
* @example Using a controllable cache
124124
* ```
125-
* const cache = new BasicClientSideCache({
126-
* ttl: 0,
127-
* maxEntries: 0,
128-
* evictPolicy: "LRU"
125+
* const cache = new BasicClientSideCache({
126+
* ttl: 0,
127+
* maxEntries: 0,
128+
* evictPolicy: "LRU"
129129
* });
130130
* const client = createClient({
131-
* RESP: 3,
131+
* RESP: 3,
132132
* clientSideCache: cache
133133
* });
134134
* ```
@@ -140,36 +140,36 @@ type WithCommands<
140140
RESP extends RespVersions,
141141
TYPE_MAPPING extends TypeMapping
142142
> = {
143-
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
144-
};
143+
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
144+
};
145145

146146
type WithModules<
147147
M extends RedisModules,
148148
RESP extends RespVersions,
149149
TYPE_MAPPING extends TypeMapping
150150
> = {
151-
[P in keyof M]: {
152-
[C in keyof M[P]]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
151+
[P in keyof M]: {
152+
[C in keyof M[P]]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
153+
};
153154
};
154-
};
155155

156156
type WithFunctions<
157157
F extends RedisFunctions,
158158
RESP extends RespVersions,
159159
TYPE_MAPPING extends TypeMapping
160160
> = {
161-
[L in keyof F]: {
162-
[C in keyof F[L]]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
161+
[L in keyof F]: {
162+
[C in keyof F[L]]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
163+
};
163164
};
164-
};
165165

166166
type WithScripts<
167167
S extends RedisScripts,
168168
RESP extends RespVersions,
169169
TYPE_MAPPING extends TypeMapping
170170
> = {
171-
[P in keyof S]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
172-
};
171+
[P in keyof S]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
172+
};
173173

174174
export type RedisClientExtensions<
175175
M extends RedisModules = {},
@@ -178,11 +178,11 @@ export type RedisClientExtensions<
178178
RESP extends RespVersions = 2,
179179
TYPE_MAPPING extends TypeMapping = {}
180180
> = (
181-
WithCommands<RESP, TYPE_MAPPING> &
182-
WithModules<M, RESP, TYPE_MAPPING> &
183-
WithFunctions<F, RESP, TYPE_MAPPING> &
184-
WithScripts<S, RESP, TYPE_MAPPING>
185-
);
181+
WithCommands<RESP, TYPE_MAPPING> &
182+
WithModules<M, RESP, TYPE_MAPPING> &
183+
WithFunctions<F, RESP, TYPE_MAPPING> &
184+
WithScripts<S, RESP, TYPE_MAPPING>
185+
);
186186

187187
export type RedisClientType<
188188
M extends RedisModules = {},
@@ -191,9 +191,9 @@ export type RedisClientType<
191191
RESP extends RespVersions = 2,
192192
TYPE_MAPPING extends TypeMapping = {}
193193
> = (
194-
RedisClient<M, F, S, RESP, TYPE_MAPPING> &
195-
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
196-
);
194+
RedisClient<M, F, S, RESP, TYPE_MAPPING> &
195+
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
196+
);
197197

198198
type ProxyClient = RedisClient<any, any, any, any, any>;
199199

@@ -353,8 +353,8 @@ export default class RedisClient<
353353
#monitorCallback?: MonitorCallback<TYPE_MAPPING>;
354354
private _self = this;
355355
private _commandOptions?: CommandOptions<TYPE_MAPPING>;
356-
// flag used to annotate that the client
357-
// was in a watch transaction when
356+
// flag used to annotate that the client
357+
// was in a watch transaction when
358358
// a topology change occured
359359
#dirtyWatch?: string;
360360
#watchEpoch?: number;
@@ -409,7 +409,7 @@ export default class RedisClient<
409409

410410
constructor(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
411411
super();
412-
412+
this.#validateOptions(options)
413413
this.#options = this.#initiateOptions(options);
414414
this.#queue = this.#initiateQueue();
415415
this.#socket = this.#initiateSocket();
@@ -425,6 +425,12 @@ export default class RedisClient<
425425
}
426426
}
427427

428+
#validateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
429+
if (options?.clientSideCache && options?.RESP !== 3) {
430+
throw new Error('Client Side Caching is only supported with RESP3');
431+
}
432+
433+
}
428434
#initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
429435

430436
// Convert username/password to credentialsProvider if no credentialsProvider is already in place
@@ -482,7 +488,7 @@ export default class RedisClient<
482488
}
483489
}
484490

485-
#subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
491+
#subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
486492
return cp.subscribe({
487493
onNext: credentials => {
488494
this.reAuthenticate(credentials).catch(error => {
@@ -517,7 +523,7 @@ export default class RedisClient<
517523

518524
if (cp && cp.type === 'streaming-credentials-provider') {
519525

520-
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
526+
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
521527
this.#credentialsSubscription = disposable;
522528

523529
if (credentials.password) {
@@ -553,7 +559,7 @@ export default class RedisClient<
553559

554560
if (cp && cp.type === 'streaming-credentials-provider') {
555561

556-
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
562+
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
557563
this.#credentialsSubscription = disposable;
558564

559565
if (credentials.username || credentials.password) {
@@ -1014,7 +1020,7 @@ export default class RedisClient<
10141020
* @internal
10151021
*/
10161022
async _executePipeline(
1017-
commands: Array<RedisMultiQueuedCommand>,
1023+
commands: Array<RedisMultiQueuedCommand>,
10181024
selectedDB?: number
10191025
) {
10201026
if (!this._self.#socket.isOpen) {
@@ -1065,8 +1071,8 @@ export default class RedisClient<
10651071
const typeMapping = this._commandOptions?.typeMapping;
10661072
const chainId = Symbol('MULTI Chain');
10671073
const promises = [
1068-
this._self.#queue.addCommand(['MULTI'], { chainId }),
1069-
];
1074+
this._self.#queue.addCommand(['MULTI'], { chainId }),
1075+
];
10701076

10711077
for (const { args } of commands) {
10721078
promises.push(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { strict as assert } from 'node:assert';
2+
import { EventEmitter } from 'node:events';
3+
import { RedisClusterOptions, RedisClusterClientOptions } from './index';
4+
import RedisClusterSlots from './cluster-slots';
5+
6+
describe('RedisClusterSlots', () => {
7+
describe('initialization', () => {
8+
9+
describe('clientSideCache validation', () => {
10+
const mockEmit = ((_event: string | symbol, ..._args: any[]): boolean => true) as EventEmitter['emit'];
11+
const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
12+
const rootNodes: Array<RedisClusterClientOptions> = [
13+
{ socket: { host: 'localhost', port: 30001 } }
14+
];
15+
16+
it('should throw error when clientSideCache is enabled with RESP 2', () => {
17+
assert.throws(
18+
() => new RedisClusterSlots({
19+
rootNodes,
20+
clientSideCache: clientSideCacheConfig,
21+
RESP: 2 as const,
22+
}, mockEmit),
23+
new Error('Client Side Caching is only supported with RESP3')
24+
);
25+
});
26+
27+
it('should throw error when clientSideCache is enabled with RESP undefined', () => {
28+
assert.throws(
29+
() => new RedisClusterSlots({
30+
rootNodes,
31+
clientSideCache: clientSideCacheConfig,
32+
}, mockEmit),
33+
new Error('Client Side Caching is only supported with RESP3')
34+
);
35+
});
36+
37+
it('should not throw when clientSideCache is enabled with RESP 3', () => {
38+
assert.doesNotThrow(() =>
39+
new RedisClusterSlots({
40+
rootNodes,
41+
clientSideCache: clientSideCacheConfig,
42+
RESP: 3 as const,
43+
}, mockEmit)
44+
);
45+
});
46+
});
47+
});
48+
});

0 commit comments

Comments
 (0)