Skip to content

Commit 8a0f6de

Browse files
committed
Add client side caching RESP3 validation
1 parent 6738027 commit 8a0f6de

File tree

7 files changed

+207
-59
lines changed

7 files changed

+207
-59
lines changed

packages/client/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,5 @@ export { SetOptions } from './lib/commands/SET';
3535

3636
export { REDIS_FLUSH_MODES } from './lib/commands/FLUSHALL';
3737

38-
import { BasicClientSideCache, BasicPooledClientSideCache } from './lib/client/cache';
39-
export { BasicClientSideCache, BasicPooledClientSideCache };
38+
export { BasicClientSideCache, BasicPooledClientSideCache } from './lib/client/cache';
4039

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

Lines changed: 49 additions & 15 deletions
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

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ export interface RedisClientOptions<
8080
pingInterval?: number;
8181
/**
8282
* Default command options to be applied to all commands executed through this client.
83-
*
83+
*
8484
* These options can be overridden on a per-command basis when calling specific commands.
85-
*
85+
*
8686
* @property {symbol} [chainId] - Identifier for chaining commands together
8787
* @property {boolean} [asap] - When true, the command is executed as soon as possible
8888
* @property {AbortSignal} [abortSignal] - AbortSignal to cancel the command
8989
* @property {TypeMapping} [typeMapping] - Custom type mappings between RESP and JavaScript types
90-
*
90+
*
9191
* @example Setting default command options
9292
* ```
9393
* const client = createClient({
@@ -103,33 +103,33 @@ export interface RedisClientOptions<
103103
commandOptions?: CommandOptions<TYPE_MAPPING>;
104104
/**
105105
* Client Side Caching configuration.
106-
*
107-
* Enables Redis Servers and Clients to work together to cache results from commands
106+
*
107+
* Enables Redis Servers and Clients to work together to cache results from commands
108108
* sent to a server. The server will notify the client when cached results are no longer valid.
109-
*
109+
*
110110
* Note: Client Side Caching is only supported with RESP3.
111-
*
111+
*
112112
* @example Anonymous cache configuration
113113
* ```
114114
* const client = createClient({
115-
* RESP: 3,
115+
* RESP: 3,
116116
* clientSideCache: {
117117
* ttl: 0,
118118
* maxEntries: 0,
119-
* evictPolicy: "LRU"
119+
* evictPolicy: "LRU"
120120
* }
121121
* });
122122
* ```
123-
*
123+
*
124124
* @example Using a controllable cache
125125
* ```
126-
* const cache = new BasicClientSideCache({
127-
* ttl: 0,
128-
* maxEntries: 0,
129-
* evictPolicy: "LRU"
126+
* const cache = new BasicClientSideCache({
127+
* ttl: 0,
128+
* maxEntries: 0,
129+
* evictPolicy: "LRU"
130130
* });
131131
* const client = createClient({
132-
* RESP: 3,
132+
* RESP: 3,
133133
* clientSideCache: cache
134134
* });
135135
* ```
@@ -141,36 +141,36 @@ type WithCommands<
141141
RESP extends RespVersions,
142142
TYPE_MAPPING extends TypeMapping
143143
> = {
144-
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
145-
};
144+
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
145+
};
146146

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

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

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

175175
export type RedisClientExtensions<
176176
M extends RedisModules = {},
@@ -179,11 +179,11 @@ export type RedisClientExtensions<
179179
RESP extends RespVersions = 2,
180180
TYPE_MAPPING extends TypeMapping = {}
181181
> = (
182-
WithCommands<RESP, TYPE_MAPPING> &
183-
WithModules<M, RESP, TYPE_MAPPING> &
184-
WithFunctions<F, RESP, TYPE_MAPPING> &
185-
WithScripts<S, RESP, TYPE_MAPPING>
186-
);
182+
WithCommands<RESP, TYPE_MAPPING> &
183+
WithModules<M, RESP, TYPE_MAPPING> &
184+
WithFunctions<F, RESP, TYPE_MAPPING> &
185+
WithScripts<S, RESP, TYPE_MAPPING>
186+
);
187187

188188
export type RedisClientType<
189189
M extends RedisModules = {},
@@ -192,9 +192,9 @@ export type RedisClientType<
192192
RESP extends RespVersions = 2,
193193
TYPE_MAPPING extends TypeMapping = {}
194194
> = (
195-
RedisClient<M, F, S, RESP, TYPE_MAPPING> &
196-
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
197-
);
195+
RedisClient<M, F, S, RESP, TYPE_MAPPING> &
196+
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
197+
);
198198

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

@@ -363,8 +363,8 @@ export default class RedisClient<
363363
#monitorCallback?: MonitorCallback<TYPE_MAPPING>;
364364
private _self = this;
365365
private _commandOptions?: CommandOptions<TYPE_MAPPING>;
366-
// flag used to annotate that the client
367-
// was in a watch transaction when
366+
// flag used to annotate that the client
367+
// was in a watch transaction when
368368
// a topology change occured
369369
#dirtyWatch?: string;
370370
#watchEpoch?: number;
@@ -419,7 +419,7 @@ export default class RedisClient<
419419

420420
constructor(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
421421
super();
422-
422+
this.#validateOptions(options)
423423
this.#options = this.#initiateOptions(options);
424424
this.#queue = this.#initiateQueue();
425425
this.#socket = this.#initiateSocket();
@@ -435,6 +435,12 @@ export default class RedisClient<
435435
}
436436
}
437437

438+
#validateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
439+
if (options?.clientSideCache && options?.RESP !== 3) {
440+
throw new Error('Client Side Caching is only supported with RESP3');
441+
}
442+
443+
}
438444
#initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
439445

440446
// Convert username/password to credentialsProvider if no credentialsProvider is already in place
@@ -492,7 +498,7 @@ export default class RedisClient<
492498
}
493499
}
494500

495-
#subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
501+
#subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
496502
return cp.subscribe({
497503
onNext: credentials => {
498504
this.reAuthenticate(credentials).catch(error => {
@@ -527,7 +533,7 @@ export default class RedisClient<
527533

528534
if (cp && cp.type === 'streaming-credentials-provider') {
529535

530-
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
536+
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
531537
this.#credentialsSubscription = disposable;
532538

533539
if (credentials.password) {
@@ -563,7 +569,7 @@ export default class RedisClient<
563569

564570
if (cp && cp.type === 'streaming-credentials-provider') {
565571

566-
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
572+
const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp)
567573
this.#credentialsSubscription = disposable;
568574

569575
if (credentials.username || credentials.password) {
@@ -1024,7 +1030,7 @@ export default class RedisClient<
10241030
* @internal
10251031
*/
10261032
async _executePipeline(
1027-
commands: Array<RedisMultiQueuedCommand>,
1033+
commands: Array<RedisMultiQueuedCommand>,
10281034
selectedDB?: number
10291035
) {
10301036
if (!this._self.#socket.isOpen) {
@@ -1075,8 +1081,8 @@ export default class RedisClient<
10751081
const typeMapping = this._commandOptions?.typeMapping;
10761082
const chainId = Symbol('MULTI Chain');
10771083
const promises = [
1078-
this._self.#queue.addCommand(['MULTI'], { chainId }),
1079-
];
1084+
this._self.#queue.addCommand(['MULTI'], { chainId }),
1085+
];
10801086

10811087
for (const { args } of commands) {
10821088
promises.push(
Lines changed: 48 additions & 0 deletions
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)