You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-07 13:22:56 +03:00
V5 bringing RESP3, Sentinel and TypeMapping to node-redis
RESP3 Support - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion. Sentinel TypeMapping Correctly types Multi commands Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [2, "always"]
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
.nyc_output/
|
||||
coverage/
|
||||
documentation/
|
||||
lib/
|
||||
.eslintrc.json
|
||||
.nycrc.json
|
||||
.release-it.json
|
||||
dump.rdb
|
||||
index.ts
|
||||
tsconfig.json
|
@@ -5,6 +5,7 @@
|
||||
"tagAnnotation": "Release ${tagName}"
|
||||
},
|
||||
"npm": {
|
||||
"versionArgs": ["--workspaces-update=false"],
|
||||
"publishArgs": ["--access", "public"]
|
||||
}
|
||||
}
|
||||
|
@@ -1,24 +1,27 @@
|
||||
import RedisClient from './lib/client';
|
||||
import RedisCluster from './lib/cluster';
|
||||
|
||||
export { RedisClientType, RedisClientOptions } from './lib/client';
|
||||
|
||||
export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands';
|
||||
export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/*, CommandPolicies*/, RedisArgument } from './lib/RESP/types';
|
||||
export { RESP_TYPES } from './lib/RESP/decoder';
|
||||
export { VerbatimString } from './lib/RESP/verbatim-string';
|
||||
export { defineScript } from './lib/lua-script';
|
||||
// export * from './lib/errors';
|
||||
|
||||
import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client';
|
||||
export { RedisClientOptions, RedisClientType };
|
||||
export const createClient = RedisClient.create;
|
||||
|
||||
export const commandOptions = RedisClient.commandOptions;
|
||||
|
||||
export { RedisClusterType, RedisClusterOptions } from './lib/cluster';
|
||||
import { RedisClientPool, RedisPoolOptions, RedisClientPoolType } from './lib/client/pool';
|
||||
export { RedisClientPoolType, RedisPoolOptions };
|
||||
export const createClientPool = RedisClientPool.create;
|
||||
|
||||
import RedisCluster, { RedisClusterOptions, RedisClusterType } from './lib/cluster';
|
||||
export { RedisClusterType, RedisClusterOptions };
|
||||
export const createCluster = RedisCluster.create;
|
||||
|
||||
export { defineScript } from './lib/lua-script';
|
||||
import RedisSentinel from './lib/sentinel';
|
||||
export { RedisSentinelOptions, RedisSentinelType } from './lib/sentinel/types';
|
||||
export const createSentinel = RedisSentinel.create;
|
||||
|
||||
export * from './lib/errors';
|
||||
// export { GeoReplyWith } from './lib/commands/generic-transformers';
|
||||
|
||||
export { GeoReplyWith } from './lib/commands/generic-transformers';
|
||||
// export { SetOptions } from './lib/commands/SET';
|
||||
|
||||
export { SetOptions } from './lib/commands/SET';
|
||||
|
||||
export { RedisFlushModes } from './lib/commands/FLUSHALL';
|
||||
// export { RedisFlushModes } from './lib/commands/FLUSHALL';
|
||||
|
426
packages/client/lib/RESP/decoder.spec.ts
Normal file
426
packages/client/lib/RESP/decoder.spec.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { SinonSpy, spy } from 'sinon';
|
||||
import { Decoder, RESP_TYPES } from './decoder';
|
||||
import { BlobError, SimpleError } from '../errors';
|
||||
import { TypeMapping } from './types';
|
||||
import { VerbatimString } from './verbatim-string';
|
||||
|
||||
interface Test {
|
||||
toWrite: Buffer;
|
||||
typeMapping?: TypeMapping;
|
||||
replies?: Array<unknown>;
|
||||
errorReplies?: Array<unknown>;
|
||||
pushReplies?: Array<unknown>;
|
||||
}
|
||||
|
||||
function test(name: string, config: Test) {
|
||||
describe(name, () => {
|
||||
it('single chunk', () => {
|
||||
const setup = setupTest(config);
|
||||
setup.decoder.write(config.toWrite);
|
||||
assertSpiesCalls(config, setup);
|
||||
});
|
||||
|
||||
it('byte by byte', () => {
|
||||
const setup = setupTest(config);
|
||||
for (let i = 0; i < config.toWrite.length; i++) {
|
||||
setup.decoder.write(config.toWrite.subarray(i, i + 1));
|
||||
}
|
||||
assertSpiesCalls(config, setup);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function setupTest(config: Test) {
|
||||
const onReplySpy = spy(),
|
||||
onErrorReplySpy = spy(),
|
||||
onPushSpy = spy();
|
||||
|
||||
return {
|
||||
decoder: new Decoder({
|
||||
getTypeMapping: () => config.typeMapping ?? {},
|
||||
onReply: onReplySpy,
|
||||
onErrorReply: onErrorReplySpy,
|
||||
onPush: onPushSpy
|
||||
}),
|
||||
onReplySpy,
|
||||
onErrorReplySpy,
|
||||
onPushSpy
|
||||
};
|
||||
}
|
||||
|
||||
function assertSpiesCalls(config: Test, spies: ReturnType<typeof setupTest>) {
|
||||
assertSpyCalls(spies.onReplySpy, config.replies);
|
||||
assertSpyCalls(spies.onErrorReplySpy, config.errorReplies);
|
||||
assertSpyCalls(spies.onPushSpy, config.pushReplies);
|
||||
}
|
||||
|
||||
function assertSpyCalls(spy: SinonSpy, replies?: Array<unknown>) {
|
||||
if (!replies) {
|
||||
assert.equal(spy.callCount, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(spy.callCount, replies.length);
|
||||
for (const [i, reply] of replies.entries()) {
|
||||
assert.deepEqual(
|
||||
spy.getCall(i).args,
|
||||
[reply]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('RESP Decoder', () => {
|
||||
test('Null', {
|
||||
toWrite: Buffer.from('_\r\n'),
|
||||
replies: [null]
|
||||
});
|
||||
|
||||
describe('Boolean', () => {
|
||||
test('true', {
|
||||
toWrite: Buffer.from('#t\r\n'),
|
||||
replies: [true]
|
||||
});
|
||||
|
||||
test('false', {
|
||||
toWrite: Buffer.from('#f\r\n'),
|
||||
replies: [false]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Number', () => {
|
||||
test('0', {
|
||||
toWrite: Buffer.from(':0\r\n'),
|
||||
replies: [0]
|
||||
});
|
||||
|
||||
test('1', {
|
||||
toWrite: Buffer.from(':+1\r\n'),
|
||||
replies: [1]
|
||||
});
|
||||
|
||||
test('+1', {
|
||||
toWrite: Buffer.from(':+1\r\n'),
|
||||
replies: [1]
|
||||
});
|
||||
|
||||
test('-1', {
|
||||
toWrite: Buffer.from(':-1\r\n'),
|
||||
replies: [-1]
|
||||
});
|
||||
|
||||
test('1 as string', {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.NUMBER]: String
|
||||
},
|
||||
toWrite: Buffer.from(':1\r\n'),
|
||||
replies: ['1']
|
||||
});
|
||||
});
|
||||
|
||||
describe('BigNumber', () => {
|
||||
test('0', {
|
||||
toWrite: Buffer.from('(0\r\n'),
|
||||
replies: [0n]
|
||||
});
|
||||
|
||||
test('1', {
|
||||
toWrite: Buffer.from('(1\r\n'),
|
||||
replies: [1n]
|
||||
});
|
||||
|
||||
test('+1', {
|
||||
toWrite: Buffer.from('(+1\r\n'),
|
||||
replies: [1n]
|
||||
});
|
||||
|
||||
test('-1', {
|
||||
toWrite: Buffer.from('(-1\r\n'),
|
||||
replies: [-1n]
|
||||
});
|
||||
|
||||
test('1 as string', {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.BIG_NUMBER]: String
|
||||
},
|
||||
toWrite: Buffer.from('(1\r\n'),
|
||||
replies: ['1']
|
||||
});
|
||||
});
|
||||
|
||||
describe('Double', () => {
|
||||
test('0', {
|
||||
toWrite: Buffer.from(',0\r\n'),
|
||||
replies: [0]
|
||||
});
|
||||
|
||||
test('1', {
|
||||
toWrite: Buffer.from(',1\r\n'),
|
||||
replies: [1]
|
||||
});
|
||||
|
||||
test('+1', {
|
||||
toWrite: Buffer.from(',+1\r\n'),
|
||||
replies: [1]
|
||||
});
|
||||
|
||||
test('-1', {
|
||||
toWrite: Buffer.from(',-1\r\n'),
|
||||
replies: [-1]
|
||||
});
|
||||
|
||||
test('1.1', {
|
||||
toWrite: Buffer.from(',1.1\r\n'),
|
||||
replies: [1.1]
|
||||
});
|
||||
|
||||
test('nan', {
|
||||
toWrite: Buffer.from(',nan\r\n'),
|
||||
replies: [NaN]
|
||||
});
|
||||
|
||||
test('inf', {
|
||||
toWrite: Buffer.from(',inf\r\n'),
|
||||
replies: [Infinity]
|
||||
});
|
||||
|
||||
test('+inf', {
|
||||
toWrite: Buffer.from(',+inf\r\n'),
|
||||
replies: [Infinity]
|
||||
});
|
||||
|
||||
test('-inf', {
|
||||
toWrite: Buffer.from(',-inf\r\n'),
|
||||
replies: [-Infinity]
|
||||
});
|
||||
|
||||
test('1e1', {
|
||||
toWrite: Buffer.from(',1e1\r\n'),
|
||||
replies: [1e1]
|
||||
});
|
||||
|
||||
test('-1.1E+1', {
|
||||
toWrite: Buffer.from(',-1.1E+1\r\n'),
|
||||
replies: [-1.1E+1]
|
||||
});
|
||||
|
||||
test('1 as string', {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.DOUBLE]: String
|
||||
},
|
||||
toWrite: Buffer.from(',1\r\n'),
|
||||
replies: ['1']
|
||||
});
|
||||
});
|
||||
|
||||
describe('SimpleString', () => {
|
||||
test("'OK'", {
|
||||
toWrite: Buffer.from('+OK\r\n'),
|
||||
replies: ['OK']
|
||||
});
|
||||
|
||||
test("'OK' as Buffer", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.SIMPLE_STRING]: Buffer
|
||||
},
|
||||
toWrite: Buffer.from('+OK\r\n'),
|
||||
replies: [Buffer.from('OK')]
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlobString', () => {
|
||||
test("''", {
|
||||
toWrite: Buffer.from('$0\r\n\r\n'),
|
||||
replies: ['']
|
||||
});
|
||||
|
||||
test("'1234567890'", {
|
||||
toWrite: Buffer.from('$10\r\n1234567890\r\n'),
|
||||
replies: ['1234567890']
|
||||
});
|
||||
|
||||
test('null (RESP2 backwards compatibility)', {
|
||||
toWrite: Buffer.from('$-1\r\n'),
|
||||
replies: [null]
|
||||
});
|
||||
|
||||
test("'OK' as Buffer", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.BLOB_STRING]: Buffer
|
||||
},
|
||||
toWrite: Buffer.from('$2\r\nOK\r\n'),
|
||||
replies: [Buffer.from('OK')]
|
||||
});
|
||||
});
|
||||
|
||||
describe('VerbatimString', () => {
|
||||
test("''", {
|
||||
toWrite: Buffer.from('=4\r\ntxt:\r\n'),
|
||||
replies: ['']
|
||||
});
|
||||
|
||||
test("'123456'", {
|
||||
toWrite: Buffer.from('=10\r\ntxt:123456\r\n'),
|
||||
replies: ['123456']
|
||||
});
|
||||
|
||||
test("'OK' as VerbatimString", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.VERBATIM_STRING]: VerbatimString
|
||||
},
|
||||
toWrite: Buffer.from('=6\r\ntxt:OK\r\n'),
|
||||
replies: [new VerbatimString('txt', 'OK')]
|
||||
});
|
||||
|
||||
test("'OK' as Buffer", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.VERBATIM_STRING]: Buffer
|
||||
},
|
||||
toWrite: Buffer.from('=6\r\ntxt:OK\r\n'),
|
||||
replies: [Buffer.from('OK')]
|
||||
});
|
||||
});
|
||||
|
||||
test('SimpleError', {
|
||||
toWrite: Buffer.from('-ERROR\r\n'),
|
||||
errorReplies: [new SimpleError('ERROR')]
|
||||
});
|
||||
|
||||
test('BlobError', {
|
||||
toWrite: Buffer.from('!5\r\nERROR\r\n'),
|
||||
errorReplies: [new BlobError('ERROR')]
|
||||
});
|
||||
|
||||
describe('Array', () => {
|
||||
test('[]', {
|
||||
toWrite: Buffer.from('*0\r\n'),
|
||||
replies: [[]]
|
||||
});
|
||||
|
||||
test('[0..9]', {
|
||||
toWrite: Buffer.from(`*10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
|
||||
replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
|
||||
});
|
||||
|
||||
test('with all types', {
|
||||
toWrite: Buffer.from([
|
||||
'*13\r\n',
|
||||
'_\r\n',
|
||||
'#f\r\n',
|
||||
':0\r\n',
|
||||
'(0\r\n',
|
||||
',0\r\n',
|
||||
'+\r\n',
|
||||
'$0\r\n\r\n',
|
||||
'=4\r\ntxt:\r\n',
|
||||
'-\r\n',
|
||||
'!0\r\n\r\n',
|
||||
'*0\r\n',
|
||||
'~0\r\n',
|
||||
'%0\r\n'
|
||||
].join('')),
|
||||
replies: [[
|
||||
null,
|
||||
false,
|
||||
0,
|
||||
0n,
|
||||
0,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
new SimpleError(''),
|
||||
new BlobError(''),
|
||||
[],
|
||||
[],
|
||||
Object.create(null)
|
||||
]]
|
||||
});
|
||||
|
||||
test('null (RESP2 backwards compatibility)', {
|
||||
toWrite: Buffer.from('*-1\r\n'),
|
||||
replies: [null]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set', () => {
|
||||
test('empty', {
|
||||
toWrite: Buffer.from('~0\r\n'),
|
||||
replies: [[]]
|
||||
});
|
||||
|
||||
test('of 0..9', {
|
||||
toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
|
||||
replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
|
||||
});
|
||||
|
||||
test('0..9 as Set', {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.SET]: Set
|
||||
},
|
||||
toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
|
||||
replies: [new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Map', () => {
|
||||
test('{}', {
|
||||
toWrite: Buffer.from('%0\r\n'),
|
||||
replies: [Object.create(null)]
|
||||
});
|
||||
|
||||
test("{ '0'..'9': <key> }", {
|
||||
toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
|
||||
replies: [Object.create(null, {
|
||||
0: { value: '0', enumerable: true },
|
||||
1: { value: '1', enumerable: true },
|
||||
2: { value: '2', enumerable: true },
|
||||
3: { value: '3', enumerable: true },
|
||||
4: { value: '4', enumerable: true },
|
||||
5: { value: '5', enumerable: true },
|
||||
6: { value: '6', enumerable: true },
|
||||
7: { value: '7', enumerable: true },
|
||||
8: { value: '8', enumerable: true },
|
||||
9: { value: '9', enumerable: true }
|
||||
})]
|
||||
});
|
||||
|
||||
test("{ '0'..'9': <key> } as Map", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.MAP]: Map
|
||||
},
|
||||
toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
|
||||
replies: [new Map([
|
||||
['0', '0'],
|
||||
['1', '1'],
|
||||
['2', '2'],
|
||||
['3', '3'],
|
||||
['4', '4'],
|
||||
['5', '5'],
|
||||
['6', '6'],
|
||||
['7', '7'],
|
||||
['8', '8'],
|
||||
['9', '9']
|
||||
])]
|
||||
});
|
||||
|
||||
test("{ '0'..'9': <key> } as Array", {
|
||||
typeMapping: {
|
||||
[RESP_TYPES.MAP]: Array
|
||||
},
|
||||
toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`),
|
||||
replies: [['0', '0', '1', '1', '2', '2', '3', '3', '4', '4', '5', '5', '6', '6', '7', '7', '8', '8', '9', '9']]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Push', () => {
|
||||
test('[]', {
|
||||
toWrite: Buffer.from('>0\r\n'),
|
||||
pushReplies: [[]]
|
||||
});
|
||||
|
||||
test('[0..9]', {
|
||||
toWrite: Buffer.from(`>10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`),
|
||||
pushReplies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
|
||||
});
|
||||
});
|
||||
});
|
1178
packages/client/lib/RESP/decoder.ts
Normal file
1178
packages/client/lib/RESP/decoder.ts
Normal file
File diff suppressed because it is too large
Load Diff
33
packages/client/lib/RESP/encoder.spec.ts
Normal file
33
packages/client/lib/RESP/encoder.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { describe } from 'mocha';
|
||||
import encodeCommand from './encoder';
|
||||
|
||||
describe('RESP Encoder', () => {
|
||||
it('1 byte', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand(['a', 'z']),
|
||||
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('2 bytes', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand(['א', 'ת']),
|
||||
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('4 bytes', () => {
|
||||
assert.deepEqual(
|
||||
[...encodeCommand(['🐣', '🐤'])],
|
||||
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('buffer', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand([Buffer.from('string')]),
|
||||
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
|
||||
);
|
||||
});
|
||||
});
|
28
packages/client/lib/RESP/encoder.ts
Normal file
28
packages/client/lib/RESP/encoder.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RedisArgument } from './types';
|
||||
|
||||
const CRLF = '\r\n';
|
||||
|
||||
export default function encodeCommand(args: Array<RedisArgument>): Array<RedisArgument> {
|
||||
const toWrite: Array<RedisArgument> = [];
|
||||
|
||||
let strings = '*' + args.length + CRLF;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (typeof arg === 'string') {
|
||||
strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
|
||||
} else if (arg instanceof Buffer) {
|
||||
toWrite.push(
|
||||
strings + '$' + arg.length.toString() + CRLF,
|
||||
arg
|
||||
);
|
||||
strings = CRLF;
|
||||
} else {
|
||||
throw new TypeError(`"arguments[${i}]" must be of type "string | Buffer", got ${typeof arg} instead.`);
|
||||
}
|
||||
}
|
||||
|
||||
toWrite.push(strings);
|
||||
|
||||
return toWrite;
|
||||
}
|
398
packages/client/lib/RESP/types.ts
Normal file
398
packages/client/lib/RESP/types.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
import { BlobError, SimpleError } from '../errors';
|
||||
import { RedisScriptConfig, SHA1 } from '../lua-script';
|
||||
import { RESP_TYPES } from './decoder';
|
||||
import { VerbatimString } from './verbatim-string';
|
||||
|
||||
export type RESP_TYPES = typeof RESP_TYPES;
|
||||
|
||||
export type RespTypes = RESP_TYPES[keyof RESP_TYPES];
|
||||
|
||||
// using interface(s) to allow circular references
|
||||
// type X = BlobStringReply | ArrayReply<X>;
|
||||
|
||||
export interface RespType<
|
||||
RESP_TYPE extends RespTypes,
|
||||
DEFAULT,
|
||||
TYPES = never,
|
||||
TYPE_MAPPING = DEFAULT | TYPES
|
||||
> {
|
||||
RESP_TYPE: RESP_TYPE;
|
||||
DEFAULT: DEFAULT;
|
||||
TYPES: TYPES;
|
||||
TYPE_MAPPING: MappedType<TYPE_MAPPING>;
|
||||
}
|
||||
|
||||
export interface NullReply extends RespType<
|
||||
RESP_TYPES['NULL'],
|
||||
null
|
||||
> {}
|
||||
|
||||
export interface BooleanReply<
|
||||
T extends boolean = boolean
|
||||
> extends RespType<
|
||||
RESP_TYPES['BOOLEAN'],
|
||||
T
|
||||
> {}
|
||||
|
||||
export interface NumberReply<
|
||||
T extends number = number
|
||||
> extends RespType<
|
||||
RESP_TYPES['NUMBER'],
|
||||
T,
|
||||
`${T}`,
|
||||
number | string
|
||||
> {}
|
||||
|
||||
export interface BigNumberReply<
|
||||
T extends bigint = bigint
|
||||
> extends RespType<
|
||||
RESP_TYPES['BIG_NUMBER'],
|
||||
T,
|
||||
number | `${T}`,
|
||||
bigint | number | string
|
||||
> {}
|
||||
|
||||
export interface DoubleReply<
|
||||
T extends number = number
|
||||
> extends RespType<
|
||||
RESP_TYPES['DOUBLE'],
|
||||
T,
|
||||
`${T}`,
|
||||
number | string
|
||||
> {}
|
||||
|
||||
export interface SimpleStringReply<
|
||||
T extends string = string
|
||||
> extends RespType<
|
||||
RESP_TYPES['SIMPLE_STRING'],
|
||||
T,
|
||||
Buffer,
|
||||
string | Buffer
|
||||
> {}
|
||||
|
||||
export interface BlobStringReply<
|
||||
T extends string = string
|
||||
> extends RespType<
|
||||
RESP_TYPES['BLOB_STRING'],
|
||||
T,
|
||||
Buffer,
|
||||
string | Buffer
|
||||
> {
|
||||
toString(): string
|
||||
}
|
||||
|
||||
export interface VerbatimStringReply<
|
||||
T extends string = string
|
||||
> extends RespType<
|
||||
RESP_TYPES['VERBATIM_STRING'],
|
||||
T,
|
||||
Buffer | VerbatimString,
|
||||
string | Buffer | VerbatimString
|
||||
> {}
|
||||
|
||||
export interface SimpleErrorReply extends RespType<
|
||||
RESP_TYPES['SIMPLE_ERROR'],
|
||||
SimpleError,
|
||||
Buffer
|
||||
> {}
|
||||
|
||||
export interface BlobErrorReply extends RespType<
|
||||
RESP_TYPES['BLOB_ERROR'],
|
||||
BlobError,
|
||||
Buffer
|
||||
> {}
|
||||
|
||||
export interface ArrayReply<T> extends RespType<
|
||||
RESP_TYPES['ARRAY'],
|
||||
Array<T>,
|
||||
never,
|
||||
Array<any>
|
||||
> {}
|
||||
|
||||
export interface TuplesReply<T extends [...Array<unknown>]> extends RespType<
|
||||
RESP_TYPES['ARRAY'],
|
||||
T,
|
||||
never,
|
||||
Array<any>
|
||||
> {}
|
||||
|
||||
export interface SetReply<T> extends RespType<
|
||||
RESP_TYPES['SET'],
|
||||
Array<T>,
|
||||
Set<T>,
|
||||
Array<any> | Set<any>
|
||||
> {}
|
||||
|
||||
export interface MapReply<K, V> extends RespType<
|
||||
RESP_TYPES['MAP'],
|
||||
{ [key: string]: V },
|
||||
Map<K, V> | Array<K | V>,
|
||||
Map<any, any> | Array<any>
|
||||
> {}
|
||||
|
||||
type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown];
|
||||
|
||||
type MapTuples = Array<MapKeyValue>;
|
||||
|
||||
type ExtractMapKey<T> = (
|
||||
T extends BlobStringReply<infer S> ? S :
|
||||
T extends SimpleStringReply<infer S> ? S :
|
||||
never
|
||||
);
|
||||
|
||||
export interface TuplesToMapReply<T extends MapTuples> extends RespType<
|
||||
RESP_TYPES['MAP'],
|
||||
{
|
||||
[P in T[number] as ExtractMapKey<P[0]>]: P[1];
|
||||
},
|
||||
Map<ExtractMapKey<T[number][0]>, T[number][1]> | FlattenTuples<T>
|
||||
> {}
|
||||
|
||||
type FlattenTuples<T> = (
|
||||
T extends [] ? [] :
|
||||
T extends [MapKeyValue] ? T[0] :
|
||||
T extends [MapKeyValue, ...infer R] ? [
|
||||
...T[0],
|
||||
...FlattenTuples<R>
|
||||
] :
|
||||
never
|
||||
);
|
||||
|
||||
export type ReplyUnion = (
|
||||
NullReply |
|
||||
BooleanReply |
|
||||
NumberReply |
|
||||
BigNumberReply |
|
||||
DoubleReply |
|
||||
SimpleStringReply |
|
||||
BlobStringReply |
|
||||
VerbatimStringReply |
|
||||
SimpleErrorReply |
|
||||
BlobErrorReply |
|
||||
ArrayReply<ReplyUnion> |
|
||||
SetReply<ReplyUnion> |
|
||||
MapReply<ReplyUnion, ReplyUnion>
|
||||
);
|
||||
|
||||
export type MappedType<T> = ((...args: any) => T) | (new (...args: any) => T);
|
||||
|
||||
type InferTypeMapping<T> = T extends RespType<RespTypes, unknown, unknown, infer FLAG_TYPES> ? FLAG_TYPES : never;
|
||||
|
||||
export type TypeMapping = {
|
||||
[P in RespTypes]?: MappedType<InferTypeMapping<Extract<ReplyUnion, RespType<P, any, any, any>>>>;
|
||||
};
|
||||
|
||||
type MapKey<
|
||||
T,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = ReplyWithTypeMapping<T, TYPE_MAPPING & {
|
||||
// simple and blob strings as map keys decoded as strings
|
||||
[RESP_TYPES.SIMPLE_STRING]: StringConstructor;
|
||||
[RESP_TYPES.BLOB_STRING]: StringConstructor;
|
||||
}>;
|
||||
|
||||
export type UnwrapReply<REPLY extends RespType<any, any, any, any>> = REPLY['DEFAULT' | 'TYPES'];
|
||||
|
||||
export type ReplyWithTypeMapping<
|
||||
REPLY,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (
|
||||
// if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
|
||||
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
|
||||
TYPE_MAPPING[RESP_TYPE] extends MappedType<infer T> ?
|
||||
ReplyWithTypeMapping<Extract<DEFAULT | TYPES, T>, TYPE_MAPPING> :
|
||||
ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
|
||||
: (
|
||||
// if REPLY is a known generic type, convert its generic arguments
|
||||
// TODO: tuples?
|
||||
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
|
||||
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
|
||||
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
|
||||
// `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
|
||||
REPLY extends Date | Buffer | Error ? REPLY :
|
||||
REPLY extends Record<PropertyKey, any> ? {
|
||||
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
|
||||
} :
|
||||
// otherwise, just return the REPLY as is
|
||||
REPLY
|
||||
)
|
||||
);
|
||||
|
||||
export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO;
|
||||
|
||||
export type RedisArgument = string | Buffer;
|
||||
|
||||
export type CommandArguments = Array<RedisArgument> & { preserve?: unknown };
|
||||
|
||||
// export const REQUEST_POLICIES = {
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// ALL_NODES: 'all_nodes',
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// ALL_SHARDS: 'all_shards',
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// SPECIAL: 'special'
|
||||
// } as const;
|
||||
|
||||
// export type REQUEST_POLICIES = typeof REQUEST_POLICIES;
|
||||
|
||||
// export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES];
|
||||
|
||||
// export const RESPONSE_POLICIES = {
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// ONE_SUCCEEDED: 'one_succeeded',
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// ALL_SUCCEEDED: 'all_succeeded',
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// LOGICAL_AND: 'agg_logical_and',
|
||||
// /**
|
||||
// * TODO
|
||||
// */
|
||||
// SPECIAL: 'special'
|
||||
// } as const;
|
||||
|
||||
// export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES;
|
||||
|
||||
// export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES];
|
||||
|
||||
// export type CommandPolicies = {
|
||||
// request?: RequestPolicies | null;
|
||||
// response?: ResponsePolicies | null;
|
||||
// };
|
||||
|
||||
export type Command = {
|
||||
FIRST_KEY_INDEX?: number | ((this: void, ...args: Array<any>) => RedisArgument | undefined);
|
||||
IS_READ_ONLY?: boolean;
|
||||
/**
|
||||
* @internal
|
||||
* TODO: remove once `POLICIES` is implemented
|
||||
*/
|
||||
IS_FORWARD_COMMAND?: boolean;
|
||||
// POLICIES?: CommandPolicies;
|
||||
transformArguments(this: void, ...args: Array<any>): CommandArguments;
|
||||
TRANSFORM_LEGACY_REPLY?: boolean;
|
||||
transformReply: TransformReply | Record<RespVersions, TransformReply>;
|
||||
unstableResp3?: boolean;
|
||||
};
|
||||
|
||||
export type RedisCommands = Record<string, Command>;
|
||||
|
||||
export type RedisModules = Record<string, RedisCommands>;
|
||||
|
||||
export interface RedisFunction extends Command {
|
||||
NUMBER_OF_KEYS?: number;
|
||||
}
|
||||
|
||||
export type RedisFunctions = Record<string, Record<string, RedisFunction>>;
|
||||
|
||||
export type RedisScript = RedisScriptConfig & SHA1;
|
||||
|
||||
export type RedisScripts = Record<string, RedisScript>;
|
||||
|
||||
// TODO: move to Commander?
|
||||
export interface CommanderConfig<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions
|
||||
> {
|
||||
modules?: M;
|
||||
functions?: F;
|
||||
scripts?: S;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
RESP?: RESP;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
unstableResp3?: boolean;
|
||||
}
|
||||
|
||||
type Resp2Array<T> = (
|
||||
T extends [] ? [] :
|
||||
T extends [infer ITEM] ? [Resp2Reply<ITEM>] :
|
||||
T extends [infer ITEM, ...infer REST] ? [
|
||||
Resp2Reply<ITEM>,
|
||||
...Resp2Array<REST>
|
||||
] :
|
||||
T extends Array<infer ITEM> ? Array<Resp2Reply<ITEM>> :
|
||||
never
|
||||
);
|
||||
|
||||
export type Resp2Reply<RESP3REPLY> = (
|
||||
RESP3REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
|
||||
// TODO: RESP3 only scalar types
|
||||
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
|
||||
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
|
||||
RESP_TYPE,
|
||||
Resp2Array<DEFAULT>
|
||||
> :
|
||||
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
|
||||
RESP_TYPES['ARRAY'],
|
||||
Resp2Array<Extract<TYPES, Array<any>>>
|
||||
> :
|
||||
RESP3REPLY :
|
||||
RESP3REPLY
|
||||
);
|
||||
|
||||
export type RespVersions = 2 | 3;
|
||||
|
||||
export type CommandReply<
|
||||
COMMAND extends Command,
|
||||
RESP extends RespVersions
|
||||
> = (
|
||||
// if transformReply is a function, use its return type
|
||||
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
|
||||
// if transformReply[RESP] is a function, use its return type
|
||||
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
|
||||
// otherwise use the generic reply type
|
||||
ReplyUnion
|
||||
);
|
||||
|
||||
export type CommandSignature<
|
||||
COMMAND extends Command,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>>;
|
||||
|
||||
// export type CommandWithPoliciesSignature<
|
||||
// COMMAND extends Command,
|
||||
// RESP extends RespVersions,
|
||||
// TYPE_MAPPING extends TypeMapping,
|
||||
// POLICIES extends CommandPolicies
|
||||
// > = (...args: Parameters<COMMAND['transformArguments']>) => Promise<
|
||||
// ReplyWithPolicy<
|
||||
// ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>,
|
||||
// MergePolicies<COMMAND, POLICIES>
|
||||
// >
|
||||
// >;
|
||||
|
||||
// export type MergePolicies<
|
||||
// COMMAND extends Command,
|
||||
// POLICIES extends CommandPolicies
|
||||
// > = Omit<COMMAND['POLICIES'], keyof POLICIES> & POLICIES;
|
||||
|
||||
// type ReplyWithPolicy<
|
||||
// REPLY,
|
||||
// POLICIES extends CommandPolicies,
|
||||
// > = (
|
||||
// POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never :
|
||||
// POLICIES['request'] extends null | undefined ? REPLY :
|
||||
// unknown extends POLICIES['request'] ? REPLY :
|
||||
// POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never :
|
||||
// POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY :
|
||||
// // otherwise, return array of replies
|
||||
// Array<REPLY>
|
||||
// );
|
8
packages/client/lib/RESP/verbatim-string.ts
Normal file
8
packages/client/lib/RESP/verbatim-string.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class VerbatimString extends String {
|
||||
constructor(
|
||||
public format: string,
|
||||
value: string
|
||||
) {
|
||||
super(value);
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import BufferComposer from './buffer';
|
||||
|
||||
describe('Buffer Composer', () => {
|
||||
const composer = new BufferComposer();
|
||||
|
||||
it('should compose two buffers', () => {
|
||||
composer.write(Buffer.from([0]));
|
||||
assert.deepEqual(
|
||||
composer.end(Buffer.from([1])),
|
||||
Buffer.from([0, 1])
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,18 +0,0 @@
|
||||
import { Composer } from './interface';
|
||||
|
||||
export default class BufferComposer implements Composer<Buffer> {
|
||||
private chunks: Array<Buffer> = [];
|
||||
|
||||
write(buffer: Buffer): void {
|
||||
this.chunks.push(buffer);
|
||||
}
|
||||
|
||||
end(buffer: Buffer): Buffer {
|
||||
this.write(buffer);
|
||||
return Buffer.concat(this.chunks.splice(0));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.chunks = [];
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
export interface Composer<T> {
|
||||
write(buffer: Buffer): void;
|
||||
|
||||
end(buffer: Buffer): T;
|
||||
|
||||
reset(): void;
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import StringComposer from './string';
|
||||
|
||||
describe('String Composer', () => {
|
||||
const composer = new StringComposer();
|
||||
|
||||
it('should compose two strings', () => {
|
||||
composer.write(Buffer.from([0]));
|
||||
assert.deepEqual(
|
||||
composer.end(Buffer.from([1])),
|
||||
Buffer.from([0, 1]).toString()
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,22 +0,0 @@
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
import { Composer } from './interface';
|
||||
|
||||
export default class StringComposer implements Composer<string> {
|
||||
private decoder = new StringDecoder();
|
||||
|
||||
private string = '';
|
||||
|
||||
write(buffer: Buffer): void {
|
||||
this.string += this.decoder.write(buffer);
|
||||
}
|
||||
|
||||
end(buffer: Buffer): string {
|
||||
const string = this.string + this.decoder.end(buffer);
|
||||
this.string = '';
|
||||
return string;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.string = '';
|
||||
}
|
||||
}
|
@@ -1,195 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { SinonSpy, spy } from 'sinon';
|
||||
import RESP2Decoder from './decoder';
|
||||
import { ErrorReply } from '../../errors';
|
||||
|
||||
interface DecoderAndSpies {
|
||||
decoder: RESP2Decoder;
|
||||
returnStringsAsBuffersSpy: SinonSpy;
|
||||
onReplySpy: SinonSpy;
|
||||
}
|
||||
|
||||
function createDecoderAndSpies(returnStringsAsBuffers: boolean): DecoderAndSpies {
|
||||
const returnStringsAsBuffersSpy = spy(() => returnStringsAsBuffers),
|
||||
onReplySpy = spy();
|
||||
|
||||
return {
|
||||
decoder: new RESP2Decoder({
|
||||
returnStringsAsBuffers: returnStringsAsBuffersSpy,
|
||||
onReply: onReplySpy
|
||||
}),
|
||||
returnStringsAsBuffersSpy,
|
||||
onReplySpy
|
||||
};
|
||||
}
|
||||
|
||||
function writeChunks(stream: RESP2Decoder, buffer: Buffer) {
|
||||
let i = 0;
|
||||
while (i < buffer.length) {
|
||||
stream.write(buffer.slice(i, ++i));
|
||||
}
|
||||
}
|
||||
|
||||
type Replies = Array<Array<unknown>>;
|
||||
|
||||
interface TestsOptions {
|
||||
toWrite: Buffer;
|
||||
returnStringsAsBuffers: boolean;
|
||||
replies: Replies;
|
||||
}
|
||||
|
||||
function generateTests({
|
||||
toWrite,
|
||||
returnStringsAsBuffers,
|
||||
replies
|
||||
}: TestsOptions): void {
|
||||
it('single chunk', () => {
|
||||
const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
|
||||
createDecoderAndSpies(returnStringsAsBuffers);
|
||||
decoder.write(toWrite);
|
||||
assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
|
||||
testReplies(onReplySpy, replies);
|
||||
});
|
||||
|
||||
it('multiple chunks', () => {
|
||||
const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
|
||||
createDecoderAndSpies(returnStringsAsBuffers);
|
||||
writeChunks(decoder, toWrite);
|
||||
assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
|
||||
testReplies(onReplySpy, replies);
|
||||
});
|
||||
}
|
||||
|
||||
function testReplies(spy: SinonSpy, replies: Replies): void {
|
||||
if (!replies) {
|
||||
assert.equal(spy.callCount, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(spy.callCount, replies.length);
|
||||
for (const [i, reply] of replies.entries()) {
|
||||
assert.deepEqual(
|
||||
spy.getCall(i).args,
|
||||
reply
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('RESP2Parser', () => {
|
||||
describe('Simple String', () => {
|
||||
describe('as strings', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('+OK\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [['OK']]
|
||||
});
|
||||
});
|
||||
|
||||
describe('as buffers', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('+OK\r\n'),
|
||||
returnStringsAsBuffers: true,
|
||||
replies: [[Buffer.from('OK')]]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('-ERR\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[new ErrorReply('ERR')]]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integer', () => {
|
||||
describe('-1', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from(':-1\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[-1]]
|
||||
});
|
||||
});
|
||||
|
||||
describe('0', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from(':0\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[0]]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bulk String', () => {
|
||||
describe('null', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('$-1\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[null]]
|
||||
});
|
||||
});
|
||||
|
||||
describe('as strings', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('$2\r\naa\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [['aa']]
|
||||
});
|
||||
});
|
||||
|
||||
describe('as buffers', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('$2\r\naa\r\n'),
|
||||
returnStringsAsBuffers: true,
|
||||
replies: [[Buffer.from('aa')]]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Array', () => {
|
||||
describe('null', () => {
|
||||
generateTests({
|
||||
toWrite: Buffer.from('*-1\r\n'),
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[null]]
|
||||
});
|
||||
});
|
||||
|
||||
const arrayBuffer = Buffer.from(
|
||||
'*5\r\n' +
|
||||
'+OK\r\n' +
|
||||
'-ERR\r\n' +
|
||||
':0\r\n' +
|
||||
'$1\r\na\r\n' +
|
||||
'*0\r\n'
|
||||
);
|
||||
|
||||
describe('as strings', () => {
|
||||
generateTests({
|
||||
toWrite: arrayBuffer,
|
||||
returnStringsAsBuffers: false,
|
||||
replies: [[[
|
||||
'OK',
|
||||
new ErrorReply('ERR'),
|
||||
0,
|
||||
'a',
|
||||
[]
|
||||
]]]
|
||||
});
|
||||
});
|
||||
|
||||
describe('as buffers', () => {
|
||||
generateTests({
|
||||
toWrite: arrayBuffer,
|
||||
returnStringsAsBuffers: true,
|
||||
replies: [[[
|
||||
Buffer.from('OK'),
|
||||
new ErrorReply('ERR'),
|
||||
0,
|
||||
Buffer.from('a'),
|
||||
[]
|
||||
]]]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,257 +0,0 @@
|
||||
import { ErrorReply } from '../../errors';
|
||||
import { Composer } from './composers/interface';
|
||||
import BufferComposer from './composers/buffer';
|
||||
import StringComposer from './composers/string';
|
||||
|
||||
// RESP2 specification
|
||||
// https://redis.io/topics/protocol
|
||||
|
||||
enum Types {
|
||||
SIMPLE_STRING = 43, // +
|
||||
ERROR = 45, // -
|
||||
INTEGER = 58, // :
|
||||
BULK_STRING = 36, // $
|
||||
ARRAY = 42 // *
|
||||
}
|
||||
|
||||
enum ASCII {
|
||||
CR = 13, // \r
|
||||
ZERO = 48,
|
||||
MINUS = 45
|
||||
}
|
||||
|
||||
export type Reply = string | Buffer | ErrorReply | number | null | Array<Reply>;
|
||||
|
||||
type ArrayReply = Array<Reply> | null;
|
||||
|
||||
export type ReturnStringsAsBuffers = () => boolean;
|
||||
|
||||
interface RESP2Options {
|
||||
returnStringsAsBuffers: ReturnStringsAsBuffers;
|
||||
onReply(reply: Reply): unknown;
|
||||
}
|
||||
|
||||
interface ArrayInProcess {
|
||||
array: Array<Reply>;
|
||||
pushCounter: number;
|
||||
}
|
||||
|
||||
// Using TypeScript `private` and not the build-in `#` to avoid __classPrivateFieldGet and __classPrivateFieldSet
|
||||
|
||||
export default class RESP2Decoder {
|
||||
constructor(private options: RESP2Options) {}
|
||||
|
||||
private cursor = 0;
|
||||
|
||||
private type?: Types;
|
||||
|
||||
private bufferComposer = new BufferComposer();
|
||||
|
||||
private stringComposer = new StringComposer();
|
||||
|
||||
private currentStringComposer: BufferComposer | StringComposer = this.stringComposer;
|
||||
|
||||
reset() {
|
||||
this.cursor = 0;
|
||||
this.type = undefined;
|
||||
this.bufferComposer.reset();
|
||||
this.stringComposer.reset();
|
||||
this.currentStringComposer = this.stringComposer;
|
||||
}
|
||||
|
||||
write(chunk: Buffer): void {
|
||||
while (this.cursor < chunk.length) {
|
||||
if (!this.type) {
|
||||
this.currentStringComposer = this.options.returnStringsAsBuffers() ?
|
||||
this.bufferComposer :
|
||||
this.stringComposer;
|
||||
|
||||
this.type = chunk[this.cursor];
|
||||
if (++this.cursor >= chunk.length) break;
|
||||
}
|
||||
|
||||
const reply = this.parseType(chunk, this.type);
|
||||
if (reply === undefined) break;
|
||||
|
||||
this.type = undefined;
|
||||
this.options.onReply(reply);
|
||||
}
|
||||
|
||||
this.cursor -= chunk.length;
|
||||
}
|
||||
|
||||
private parseType(chunk: Buffer, type: Types, arraysToKeep?: number): Reply | undefined {
|
||||
switch (type) {
|
||||
case Types.SIMPLE_STRING:
|
||||
return this.parseSimpleString(chunk);
|
||||
|
||||
case Types.ERROR:
|
||||
return this.parseError(chunk);
|
||||
|
||||
case Types.INTEGER:
|
||||
return this.parseInteger(chunk);
|
||||
|
||||
case Types.BULK_STRING:
|
||||
return this.parseBulkString(chunk);
|
||||
|
||||
case Types.ARRAY:
|
||||
return this.parseArray(chunk, arraysToKeep);
|
||||
}
|
||||
}
|
||||
|
||||
private compose<
|
||||
C extends Composer<T>,
|
||||
T = C extends Composer<infer TT> ? TT : never
|
||||
>(
|
||||
chunk: Buffer,
|
||||
composer: C
|
||||
): T | undefined {
|
||||
for (let i = this.cursor; i < chunk.length; i++) {
|
||||
if (chunk[i] === ASCII.CR) {
|
||||
const reply = composer.end(
|
||||
chunk.subarray(this.cursor, i)
|
||||
);
|
||||
this.cursor = i + 2;
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
|
||||
const toWrite = chunk.subarray(this.cursor);
|
||||
composer.write(toWrite);
|
||||
this.cursor = chunk.length;
|
||||
}
|
||||
|
||||
private parseSimpleString(chunk: Buffer): string | Buffer | undefined {
|
||||
return this.compose(chunk, this.currentStringComposer);
|
||||
}
|
||||
|
||||
private parseError(chunk: Buffer): ErrorReply | undefined {
|
||||
const message = this.compose(chunk, this.stringComposer);
|
||||
if (message !== undefined) {
|
||||
return new ErrorReply(message);
|
||||
}
|
||||
}
|
||||
|
||||
private integer = 0;
|
||||
|
||||
private isNegativeInteger?: boolean;
|
||||
|
||||
private parseInteger(chunk: Buffer): number | undefined {
|
||||
if (this.isNegativeInteger === undefined) {
|
||||
this.isNegativeInteger = chunk[this.cursor] === ASCII.MINUS;
|
||||
if (this.isNegativeInteger && ++this.cursor === chunk.length) return;
|
||||
}
|
||||
|
||||
do {
|
||||
const byte = chunk[this.cursor];
|
||||
if (byte === ASCII.CR) {
|
||||
const integer = this.isNegativeInteger ? -this.integer : this.integer;
|
||||
this.integer = 0;
|
||||
this.isNegativeInteger = undefined;
|
||||
this.cursor += 2;
|
||||
return integer;
|
||||
}
|
||||
|
||||
this.integer = this.integer * 10 + byte - ASCII.ZERO;
|
||||
} while (++this.cursor < chunk.length);
|
||||
}
|
||||
|
||||
private bulkStringRemainingLength?: number;
|
||||
|
||||
private parseBulkString(chunk: Buffer): string | Buffer | null | undefined {
|
||||
if (this.bulkStringRemainingLength === undefined) {
|
||||
const length = this.parseInteger(chunk);
|
||||
if (length === undefined) return;
|
||||
if (length === -1) return null;
|
||||
|
||||
this.bulkStringRemainingLength = length;
|
||||
|
||||
if (this.cursor >= chunk.length) return;
|
||||
}
|
||||
|
||||
const end = this.cursor + this.bulkStringRemainingLength;
|
||||
if (chunk.length >= end) {
|
||||
const reply = this.currentStringComposer.end(
|
||||
chunk.subarray(this.cursor, end)
|
||||
);
|
||||
this.bulkStringRemainingLength = undefined;
|
||||
this.cursor = end + 2;
|
||||
return reply;
|
||||
}
|
||||
|
||||
const toWrite = chunk.subarray(this.cursor);
|
||||
this.currentStringComposer.write(toWrite);
|
||||
this.bulkStringRemainingLength -= toWrite.length;
|
||||
this.cursor = chunk.length;
|
||||
}
|
||||
|
||||
private arraysInProcess: Array<ArrayInProcess> = [];
|
||||
|
||||
private initializeArray = false;
|
||||
|
||||
private arrayItemType?: Types;
|
||||
|
||||
private parseArray(chunk: Buffer, arraysToKeep = 0): ArrayReply | undefined {
|
||||
if (this.initializeArray || this.arraysInProcess.length === arraysToKeep) {
|
||||
const length = this.parseInteger(chunk);
|
||||
if (length === undefined) {
|
||||
this.initializeArray = true;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.initializeArray = false;
|
||||
this.arrayItemType = undefined;
|
||||
|
||||
if (length === -1) {
|
||||
return this.returnArrayReply(null, arraysToKeep, chunk);
|
||||
} else if (length === 0) {
|
||||
return this.returnArrayReply([], arraysToKeep, chunk);
|
||||
}
|
||||
|
||||
this.arraysInProcess.push({
|
||||
array: new Array(length),
|
||||
pushCounter: 0
|
||||
});
|
||||
}
|
||||
|
||||
while (this.cursor < chunk.length) {
|
||||
if (!this.arrayItemType) {
|
||||
this.arrayItemType = chunk[this.cursor];
|
||||
|
||||
if (++this.cursor >= chunk.length) break;
|
||||
}
|
||||
|
||||
const item = this.parseType(
|
||||
chunk,
|
||||
this.arrayItemType,
|
||||
arraysToKeep + 1
|
||||
);
|
||||
if (item === undefined) break;
|
||||
|
||||
this.arrayItemType = undefined;
|
||||
|
||||
const reply = this.pushArrayItem(item, arraysToKeep);
|
||||
if (reply !== undefined) return reply;
|
||||
}
|
||||
}
|
||||
|
||||
private returnArrayReply(reply: ArrayReply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
|
||||
if (this.arraysInProcess.length <= arraysToKeep) return reply;
|
||||
|
||||
return this.pushArrayItem(reply, arraysToKeep, chunk);
|
||||
}
|
||||
|
||||
private pushArrayItem(item: Reply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
|
||||
const to = this.arraysInProcess[this.arraysInProcess.length - 1]!;
|
||||
to.array[to.pushCounter] = item;
|
||||
if (++to.pushCounter === to.array.length) {
|
||||
return this.returnArrayReply(
|
||||
this.arraysInProcess.pop()!.array,
|
||||
arraysToKeep,
|
||||
chunk
|
||||
);
|
||||
} else if (chunk && chunk.length > this.cursor) {
|
||||
return this.parseArray(chunk, arraysToKeep);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describe } from 'mocha';
|
||||
import encodeCommand from './encoder';
|
||||
|
||||
describe('RESP2 Encoder', () => {
|
||||
it('1 byte', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand(['a', 'z']),
|
||||
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('2 bytes', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand(['א', 'ת']),
|
||||
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('4 bytes', () => {
|
||||
assert.deepEqual(
|
||||
[...encodeCommand(['🐣', '🐤'])],
|
||||
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
|
||||
);
|
||||
});
|
||||
|
||||
it('buffer', () => {
|
||||
assert.deepEqual(
|
||||
encodeCommand([Buffer.from('string')]),
|
||||
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,28 +0,0 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '../../commands';
|
||||
|
||||
const CRLF = '\r\n';
|
||||
|
||||
export default function encodeCommand(args: RedisCommandArguments): Array<RedisCommandArgument> {
|
||||
const toWrite: Array<RedisCommandArgument> = [];
|
||||
|
||||
let strings = '*' + args.length + CRLF;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (typeof arg === 'string') {
|
||||
strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
|
||||
} else if (arg instanceof Buffer) {
|
||||
toWrite.push(
|
||||
strings + '$' + arg.length.toString() + CRLF,
|
||||
arg
|
||||
);
|
||||
strings = CRLF;
|
||||
} else {
|
||||
throw new TypeError('Invalid argument type');
|
||||
}
|
||||
}
|
||||
|
||||
toWrite.push(strings);
|
||||
|
||||
return toWrite;
|
||||
}
|
@@ -1,263 +1,426 @@
|
||||
import * as LinkedList from 'yallist';
|
||||
import { AbortError, ErrorReply } from '../errors';
|
||||
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
|
||||
import RESP2Decoder from './RESP2/decoder';
|
||||
import encodeCommand from './RESP2/encoder';
|
||||
import { SinglyLinkedList, DoublyLinkedNode, DoublyLinkedList } from './linked-list';
|
||||
import encodeCommand from '../RESP/encoder';
|
||||
import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder';
|
||||
import { CommandArguments, TypeMapping, ReplyUnion, RespVersions } from '../RESP/types';
|
||||
import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub';
|
||||
import { AbortError, ErrorReply } from '../errors';
|
||||
import { MonitorCallback } from '.';
|
||||
|
||||
export interface QueueCommandOptions {
|
||||
asap?: boolean;
|
||||
chainId?: symbol;
|
||||
signal?: AbortSignal;
|
||||
returnBuffers?: boolean;
|
||||
export interface CommandOptions<T = TypeMapping> {
|
||||
chainId?: symbol;
|
||||
asap?: boolean;
|
||||
abortSignal?: AbortSignal;
|
||||
/**
|
||||
* Maps between RESP and JavaScript types
|
||||
*/
|
||||
typeMapping?: T;
|
||||
}
|
||||
|
||||
export interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
||||
args: RedisCommandArguments;
|
||||
chainId?: symbol;
|
||||
abort?: {
|
||||
signal: AbortSignal;
|
||||
listener(): void;
|
||||
};
|
||||
export interface CommandToWrite extends CommandWaitingForReply {
|
||||
args: CommandArguments;
|
||||
chainId: symbol | undefined;
|
||||
abort: {
|
||||
signal: AbortSignal;
|
||||
listener: () => unknown;
|
||||
} | undefined;
|
||||
}
|
||||
|
||||
interface CommandWaitingForReply {
|
||||
resolve(reply?: unknown): void;
|
||||
reject(err: unknown): void;
|
||||
channelsCounter?: number;
|
||||
returnBuffers?: boolean;
|
||||
resolve(reply?: unknown): void;
|
||||
reject(err: unknown): void;
|
||||
channelsCounter: number | undefined;
|
||||
typeMapping: TypeMapping | undefined;
|
||||
}
|
||||
|
||||
const PONG = Buffer.from('pong');
|
||||
|
||||
export type OnShardedChannelMoved = (channel: string, listeners: ChannelListeners) => void;
|
||||
|
||||
const PONG = Buffer.from('pong'),
|
||||
RESET = Buffer.from('RESET');
|
||||
|
||||
const RESP2_PUSH_TYPE_MAPPING = {
|
||||
...PUSH_TYPE_MAPPING,
|
||||
[RESP_TYPES.SIMPLE_STRING]: Buffer
|
||||
};
|
||||
|
||||
export default class RedisCommandsQueue {
|
||||
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
|
||||
while (queue.length) {
|
||||
queue.shift()!.reject(err);
|
||||
}
|
||||
readonly #respVersion;
|
||||
readonly #maxLength;
|
||||
readonly #toWrite = new DoublyLinkedList<CommandToWrite>();
|
||||
readonly #waitingForReply = new SinglyLinkedList<CommandWaitingForReply>();
|
||||
readonly #onShardedChannelMoved;
|
||||
#chainInExecution: symbol | undefined;
|
||||
readonly decoder;
|
||||
readonly #pubSub = new PubSub();
|
||||
|
||||
get isPubSubActive() {
|
||||
return this.#pubSub.isActive;
|
||||
}
|
||||
|
||||
constructor(
|
||||
respVersion: RespVersions,
|
||||
maxLength: number | null | undefined,
|
||||
onShardedChannelMoved: OnShardedChannelMoved
|
||||
) {
|
||||
this.#respVersion = respVersion;
|
||||
this.#maxLength = maxLength;
|
||||
this.#onShardedChannelMoved = onShardedChannelMoved;
|
||||
this.decoder = this.#initiateDecoder();
|
||||
}
|
||||
|
||||
#onReply(reply: ReplyUnion) {
|
||||
this.#waitingForReply.shift()!.resolve(reply);
|
||||
}
|
||||
|
||||
#onErrorReply(err: ErrorReply) {
|
||||
this.#waitingForReply.shift()!.reject(err);
|
||||
}
|
||||
|
||||
#onPush(push: Array<any>) {
|
||||
// TODO: type
|
||||
if (this.#pubSub.handleMessageReply(push)) return true;
|
||||
|
||||
const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(push);
|
||||
if (isShardedUnsubscribe && !this.#waitingForReply.length) {
|
||||
const channel = push[1].toString();
|
||||
this.#onShardedChannelMoved(
|
||||
channel,
|
||||
this.#pubSub.removeShardedListeners(channel)
|
||||
);
|
||||
return true;
|
||||
} else if (isShardedUnsubscribe || PubSub.isStatusReply(push)) {
|
||||
const head = this.#waitingForReply.head!.value;
|
||||
if (
|
||||
(Number.isNaN(head.channelsCounter!) && push[2] === 0) ||
|
||||
--head.channelsCounter! === 0
|
||||
) {
|
||||
this.#waitingForReply.shift()!.resolve();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
readonly #maxLength: number | null | undefined;
|
||||
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
|
||||
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
|
||||
readonly #onShardedChannelMoved: OnShardedChannelMoved;
|
||||
#getTypeMapping() {
|
||||
return this.#waitingForReply.head!.value.typeMapping ?? {};
|
||||
}
|
||||
|
||||
readonly #pubSub = new PubSub();
|
||||
#initiateDecoder() {
|
||||
return new Decoder({
|
||||
onReply: reply => this.#onReply(reply),
|
||||
onErrorReply: err => this.#onErrorReply(err),
|
||||
onPush: push => {
|
||||
if (!this.#onPush(push)) {
|
||||
|
||||
get isPubSubActive() {
|
||||
return this.#pubSub.isActive;
|
||||
}
|
||||
|
||||
#chainInExecution: symbol | undefined;
|
||||
|
||||
#decoder = new RESP2Decoder({
|
||||
returnStringsAsBuffers: () => {
|
||||
return !!this.#waitingForReply.head?.value.returnBuffers ||
|
||||
this.#pubSub.isActive;
|
||||
},
|
||||
onReply: reply => {
|
||||
if (this.#pubSub.isActive && Array.isArray(reply)) {
|
||||
if (this.#pubSub.handleMessageReply(reply as Array<Buffer>)) return;
|
||||
|
||||
const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(reply as Array<Buffer>);
|
||||
if (isShardedUnsubscribe && !this.#waitingForReply.length) {
|
||||
const channel = (reply[1] as Buffer).toString();
|
||||
this.#onShardedChannelMoved(
|
||||
channel,
|
||||
this.#pubSub.removeShardedListeners(channel)
|
||||
);
|
||||
return;
|
||||
} else if (isShardedUnsubscribe || PubSub.isStatusReply(reply as Array<Buffer>)) {
|
||||
const head = this.#waitingForReply.head!.value;
|
||||
if (
|
||||
(Number.isNaN(head.channelsCounter!) && reply[2] === 0) ||
|
||||
--head.channelsCounter! === 0
|
||||
) {
|
||||
this.#waitingForReply.shift()!.resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (PONG.equals(reply[0] as Buffer)) {
|
||||
const { resolve, returnBuffers } = this.#waitingForReply.shift()!,
|
||||
buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
|
||||
resolve(returnBuffers ? buffer : buffer.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { resolve, reject } = this.#waitingForReply.shift()!;
|
||||
if (reply instanceof ErrorReply) {
|
||||
reject(reply);
|
||||
} else {
|
||||
resolve(reply);
|
||||
}
|
||||
}
|
||||
},
|
||||
getTypeMapping: () => this.#getTypeMapping()
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
maxLength: number | null | undefined,
|
||||
onShardedChannelMoved: OnShardedChannelMoved
|
||||
) {
|
||||
this.#maxLength = maxLength;
|
||||
this.#onShardedChannelMoved = onShardedChannelMoved;
|
||||
addCommand<T>(
|
||||
args: CommandArguments,
|
||||
options?: CommandOptions
|
||||
): Promise<T> {
|
||||
if (this.#maxLength && this.#toWrite.length + this.#waitingForReply.length >= this.#maxLength) {
|
||||
return Promise.reject(new Error('The queue is full'));
|
||||
} else if (options?.abortSignal?.aborted) {
|
||||
return Promise.reject(new AbortError());
|
||||
}
|
||||
|
||||
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions): Promise<T> {
|
||||
if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
|
||||
return Promise.reject(new Error('The queue is full'));
|
||||
} else if (options?.signal?.aborted) {
|
||||
return Promise.reject(new AbortError());
|
||||
return new Promise((resolve, reject) => {
|
||||
let node: DoublyLinkedNode<CommandToWrite>;
|
||||
const value: CommandToWrite = {
|
||||
args,
|
||||
chainId: options?.chainId,
|
||||
abort: undefined,
|
||||
resolve,
|
||||
reject,
|
||||
channelsCounter: undefined,
|
||||
typeMapping: options?.typeMapping
|
||||
};
|
||||
|
||||
const signal = options?.abortSignal;
|
||||
if (signal) {
|
||||
value.abort = {
|
||||
signal,
|
||||
listener: () => {
|
||||
this.#toWrite.remove(node);
|
||||
value.reject(new AbortError());
|
||||
}
|
||||
};
|
||||
signal.addEventListener('abort', value.abort.listener, { once: true });
|
||||
}
|
||||
|
||||
node = this.#toWrite.add(value, options?.asap);
|
||||
});
|
||||
}
|
||||
|
||||
#addPubSubCommand(command: PubSubCommand, asap = false, chainId?: symbol) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.#toWrite.add({
|
||||
args: command.args,
|
||||
chainId,
|
||||
abort: undefined,
|
||||
resolve() {
|
||||
command.resolve();
|
||||
resolve();
|
||||
},
|
||||
reject(err) {
|
||||
command.reject?.();
|
||||
reject(err);
|
||||
},
|
||||
channelsCounter: command.channelsCounter,
|
||||
typeMapping: PUSH_TYPE_MAPPING
|
||||
}, asap);
|
||||
});
|
||||
}
|
||||
|
||||
#setupPubSubHandler() {
|
||||
// RESP3 uses `onPush` to handle PubSub, so no need to modify `onReply`
|
||||
if (this.#respVersion !== 2) return;
|
||||
|
||||
this.decoder.onReply = (reply => {
|
||||
if (Array.isArray(reply)) {
|
||||
if (this.#onPush(reply)) return;
|
||||
|
||||
if (PONG.equals(reply[0] as Buffer)) {
|
||||
const { resolve, typeMapping } = this.#waitingForReply.shift()!,
|
||||
buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
|
||||
resolve(typeMapping?.[RESP_TYPES.SIMPLE_STRING] === Buffer ? buffer : buffer.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const node = new LinkedList.Node<CommandWaitingToBeSent>({
|
||||
args,
|
||||
chainId: options?.chainId,
|
||||
returnBuffers: options?.returnBuffers,
|
||||
resolve,
|
||||
reject
|
||||
});
|
||||
return this.#onReply(reply);
|
||||
}) as Decoder['onReply'];
|
||||
this.decoder.getTypeMapping = () => RESP2_PUSH_TYPE_MAPPING;
|
||||
}
|
||||
|
||||
if (options?.signal) {
|
||||
const listener = () => {
|
||||
this.#waitingToBeSent.removeNode(node);
|
||||
node.value.reject(new AbortError());
|
||||
};
|
||||
node.value.abort = {
|
||||
signal: options.signal,
|
||||
listener
|
||||
};
|
||||
// AbortSignal type is incorrent
|
||||
(options.signal as any).addEventListener('abort', listener, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
subscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels: string | Array<string>,
|
||||
listener: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const command = this.#pubSub.subscribe(type, channels, listener, returnBuffers);
|
||||
if (!command) return;
|
||||
|
||||
if (options?.asap) {
|
||||
this.#waitingToBeSent.unshiftNode(node);
|
||||
} else {
|
||||
this.#waitingToBeSent.pushNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.#setupPubSubHandler();
|
||||
return this.#addPubSubCommand(command);
|
||||
}
|
||||
|
||||
subscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels: string | Array<string>,
|
||||
listener: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
return this.#pushPubSubCommand(
|
||||
this.#pubSub.subscribe(type, channels, listener, returnBuffers)
|
||||
);
|
||||
}
|
||||
#resetDecoderCallbacks() {
|
||||
this.decoder.onReply = (reply => this.#onReply(reply)) as Decoder['onReply'];
|
||||
this.decoder.getTypeMapping = () => this.#getTypeMapping();
|
||||
}
|
||||
|
||||
unsubscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels?: string | Array<string>,
|
||||
listener?: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
return this.#pushPubSubCommand(
|
||||
this.#pubSub.unsubscribe(type, channels, listener, returnBuffers)
|
||||
);
|
||||
}
|
||||
unsubscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels?: string | Array<string>,
|
||||
listener?: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const command = this.#pubSub.unsubscribe(type, channels, listener, returnBuffers);
|
||||
if (!command) return;
|
||||
|
||||
resubscribe(): Promise<any> | undefined {
|
||||
const commands = this.#pubSub.resubscribe();
|
||||
if (!commands.length) return;
|
||||
|
||||
return Promise.all(
|
||||
commands.map(command => this.#pushPubSubCommand(command))
|
||||
);
|
||||
}
|
||||
|
||||
extendPubSubChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
return this.#pushPubSubCommand(
|
||||
this.#pubSub.extendChannelListeners(type, channel, listeners)
|
||||
);
|
||||
}
|
||||
|
||||
extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||
return this.#pushPubSubCommand(
|
||||
this.#pubSub.extendTypeListeners(type, listeners)
|
||||
);
|
||||
}
|
||||
|
||||
getPubSubListeners(type: PubSubType) {
|
||||
return this.#pubSub.getTypeListeners(type);
|
||||
}
|
||||
|
||||
#pushPubSubCommand(command: PubSubCommand) {
|
||||
if (command === undefined) return;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.#waitingToBeSent.push({
|
||||
args: command.args,
|
||||
channelsCounter: command.channelsCounter,
|
||||
returnBuffers: true,
|
||||
resolve: () => {
|
||||
command.resolve();
|
||||
resolve();
|
||||
},
|
||||
reject: err => {
|
||||
command.reject?.();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCommandToSend(): RedisCommandArguments | undefined {
|
||||
const toSend = this.#waitingToBeSent.shift();
|
||||
if (!toSend) return;
|
||||
|
||||
let encoded: RedisCommandArguments;
|
||||
try {
|
||||
encoded = encodeCommand(toSend.args);
|
||||
} catch (err) {
|
||||
toSend.reject(err);
|
||||
return;
|
||||
if (command && this.#respVersion === 2) {
|
||||
// RESP2 modifies `onReply` to handle PubSub (see #setupPubSubHandler)
|
||||
const { resolve } = command;
|
||||
command.resolve = () => {
|
||||
if (!this.#pubSub.isActive) {
|
||||
this.#resetDecoderCallbacks();
|
||||
}
|
||||
|
||||
this.#waitingForReply.push({
|
||||
resolve: toSend.resolve,
|
||||
reject: toSend.reject,
|
||||
channelsCounter: toSend.channelsCounter,
|
||||
returnBuffers: toSend.returnBuffers
|
||||
});
|
||||
this.#chainInExecution = toSend.chainId;
|
||||
return encoded;
|
||||
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
|
||||
onReplyChunk(chunk: Buffer): void {
|
||||
this.#decoder.write(chunk);
|
||||
}
|
||||
return this.#addPubSubCommand(command);
|
||||
}
|
||||
|
||||
flushWaitingForReply(err: Error): void {
|
||||
this.#decoder.reset();
|
||||
this.#pubSub.reset();
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||
resubscribe(chainId?: symbol) {
|
||||
const commands = this.#pubSub.resubscribe();
|
||||
if (!commands.length) return;
|
||||
|
||||
if (!this.#chainInExecution) return;
|
||||
this.#setupPubSubHandler();
|
||||
return Promise.all(
|
||||
commands.map(command => this.#addPubSubCommand(command, true, chainId))
|
||||
);
|
||||
}
|
||||
|
||||
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
|
||||
this.#waitingToBeSent.shift();
|
||||
extendPubSubChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
const command = this.#pubSub.extendChannelListeners(type, channel, listeners);
|
||||
if (!command) return;
|
||||
|
||||
this.#setupPubSubHandler();
|
||||
return this.#addPubSubCommand(command);
|
||||
}
|
||||
|
||||
extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||
const command = this.#pubSub.extendTypeListeners(type, listeners);
|
||||
if (!command) return;
|
||||
|
||||
this.#setupPubSubHandler();
|
||||
return this.#addPubSubCommand(command);
|
||||
}
|
||||
|
||||
getPubSubListeners(type: PubSubType) {
|
||||
return this.#pubSub.listeners[type];
|
||||
}
|
||||
|
||||
monitor(callback: MonitorCallback, options?: CommandOptions) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const typeMapping = options?.typeMapping ?? {};
|
||||
this.#toWrite.add({
|
||||
args: ['MONITOR'],
|
||||
chainId: options?.chainId,
|
||||
abort: undefined,
|
||||
// using `resolve` instead of using `.then`/`await` to make sure it'll be called before processing the next reply
|
||||
resolve: () => {
|
||||
// after running `MONITOR` only `MONITOR` and `RESET` replies are expected
|
||||
// any other command should cause an error
|
||||
|
||||
// if `RESET` already overrides `onReply`, set monitor as it's fallback
|
||||
if (this.#resetFallbackOnReply) {
|
||||
this.#resetFallbackOnReply = callback;
|
||||
} else {
|
||||
this.decoder.onReply = callback;
|
||||
}
|
||||
|
||||
this.decoder.getTypeMapping = () => typeMapping;
|
||||
resolve();
|
||||
},
|
||||
reject,
|
||||
channelsCounter: undefined,
|
||||
typeMapping
|
||||
}, options?.asap);
|
||||
});
|
||||
}
|
||||
|
||||
resetDecoder() {
|
||||
this.#resetDecoderCallbacks();
|
||||
this.decoder.reset();
|
||||
}
|
||||
|
||||
#resetFallbackOnReply?: Decoder['onReply'];
|
||||
|
||||
async reset<T extends TypeMapping>(chainId: symbol, typeMapping?: T) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// overriding onReply to handle `RESET` while in `MONITOR` or PubSub mode
|
||||
this.#resetFallbackOnReply = this.decoder.onReply;
|
||||
this.decoder.onReply = (reply => {
|
||||
if (
|
||||
(typeof reply === 'string' && reply === 'RESET') ||
|
||||
(reply instanceof Buffer && RESET.equals(reply))
|
||||
) {
|
||||
this.#resetDecoderCallbacks();
|
||||
this.#resetFallbackOnReply = undefined;
|
||||
this.#pubSub.reset();
|
||||
|
||||
this.#waitingForReply.shift()!.resolve(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#resetFallbackOnReply!(reply);
|
||||
}) as Decoder['onReply'];
|
||||
|
||||
this.#chainInExecution = undefined;
|
||||
this.#toWrite.push({
|
||||
args: ['RESET'],
|
||||
chainId,
|
||||
abort: undefined,
|
||||
resolve,
|
||||
reject,
|
||||
channelsCounter: undefined,
|
||||
typeMapping
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isWaitingToWrite() {
|
||||
return this.#toWrite.length > 0;
|
||||
}
|
||||
|
||||
*commandsToWrite() {
|
||||
let toSend = this.#toWrite.shift();
|
||||
while (toSend) {
|
||||
let encoded: CommandArguments;
|
||||
try {
|
||||
encoded = encodeCommand(toSend.args);
|
||||
} catch (err) {
|
||||
toSend.reject(err);
|
||||
toSend = this.#toWrite.shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO reuse `toSend` or create new object?
|
||||
(toSend as any).args = undefined;
|
||||
if (toSend.abort) {
|
||||
RedisCommandsQueue.#removeAbortListener(toSend);
|
||||
toSend.abort = undefined;
|
||||
}
|
||||
this.#chainInExecution = toSend.chainId;
|
||||
toSend.chainId = undefined;
|
||||
this.#waitingForReply.push(toSend);
|
||||
|
||||
yield encoded;
|
||||
toSend = this.#toWrite.shift();
|
||||
}
|
||||
}
|
||||
|
||||
#flushWaitingForReply(err: Error): void {
|
||||
for (const node of this.#waitingForReply) {
|
||||
node.reject(err);
|
||||
}
|
||||
this.#waitingForReply.reset();
|
||||
}
|
||||
|
||||
static #removeAbortListener(command: CommandToWrite) {
|
||||
command.abort!.signal.removeEventListener('abort', command.abort!.listener);
|
||||
}
|
||||
|
||||
static #flushToWrite(toBeSent: CommandToWrite, err: Error) {
|
||||
if (toBeSent.abort) {
|
||||
RedisCommandsQueue.#removeAbortListener(toBeSent);
|
||||
}
|
||||
|
||||
toBeSent.reject(err);
|
||||
}
|
||||
|
||||
flushWaitingForReply(err: Error): void {
|
||||
this.resetDecoder();
|
||||
this.#pubSub.reset();
|
||||
|
||||
this.#flushWaitingForReply(err);
|
||||
|
||||
if (!this.#chainInExecution) return;
|
||||
|
||||
while (this.#toWrite.head?.value.chainId === this.#chainInExecution) {
|
||||
RedisCommandsQueue.#flushToWrite(
|
||||
this.#toWrite.shift()!,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
flushAll(err: Error): void {
|
||||
this.#decoder.reset();
|
||||
this.#pubSub.reset();
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
|
||||
this.#chainInExecution = undefined;
|
||||
}
|
||||
|
||||
flushAll(err: Error): void {
|
||||
this.resetDecoder();
|
||||
this.#pubSub.reset();
|
||||
this.#flushWaitingForReply(err);
|
||||
for (const node of this.#toWrite) {
|
||||
RedisCommandsQueue.#flushToWrite(node, err);
|
||||
}
|
||||
this.#toWrite.reset();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return (
|
||||
this.#toWrite.length === 0 &&
|
||||
this.#waitingForReply.length === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,374 +0,0 @@
|
||||
import CLUSTER_COMMANDS from '../cluster/commands';
|
||||
import * as ACL_CAT from '../commands/ACL_CAT';
|
||||
import * as ACL_DELUSER from '../commands/ACL_DELUSER';
|
||||
import * as ACL_DRYRUN from '../commands/ACL_DRYRUN';
|
||||
import * as ACL_GENPASS from '../commands/ACL_GENPASS';
|
||||
import * as ACL_GETUSER from '../commands/ACL_GETUSER';
|
||||
import * as ACL_LIST from '../commands/ACL_LIST';
|
||||
import * as ACL_LOAD from '../commands/ACL_LOAD';
|
||||
import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET';
|
||||
import * as ACL_LOG from '../commands/ACL_LOG';
|
||||
import * as ACL_SAVE from '../commands/ACL_SAVE';
|
||||
import * as ACL_SETUSER from '../commands/ACL_SETUSER';
|
||||
import * as ACL_USERS from '../commands/ACL_USERS';
|
||||
import * as ACL_WHOAMI from '../commands/ACL_WHOAMI';
|
||||
import * as ASKING from '../commands/ASKING';
|
||||
import * as AUTH from '../commands/AUTH';
|
||||
import * as BGREWRITEAOF from '../commands/BGREWRITEAOF';
|
||||
import * as BGSAVE from '../commands/BGSAVE';
|
||||
import * as CLIENT_CACHING from '../commands/CLIENT_CACHING';
|
||||
import * as CLIENT_GETNAME from '../commands/CLIENT_GETNAME';
|
||||
import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR';
|
||||
import * as CLIENT_ID from '../commands/CLIENT_ID';
|
||||
import * as CLIENT_KILL from '../commands/CLIENT_KILL';
|
||||
import * as CLIENT_LIST from '../commands/CLIENT_LIST';
|
||||
import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT';
|
||||
import * as CLIENT_NO_TOUCH from '../commands/CLIENT_NO-TOUCH';
|
||||
import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE';
|
||||
import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME';
|
||||
import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING';
|
||||
import * as CLIENT_TRACKINGINFO from '../commands/CLIENT_TRACKINGINFO';
|
||||
import * as CLIENT_UNPAUSE from '../commands/CLIENT_UNPAUSE';
|
||||
import * as CLIENT_INFO from '../commands/CLIENT_INFO';
|
||||
import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS';
|
||||
import * as CLUSTER_ADDSLOTSRANGE from '../commands/CLUSTER_ADDSLOTSRANGE';
|
||||
import * as CLUSTER_BUMPEPOCH from '../commands/CLUSTER_BUMPEPOCH';
|
||||
import * as CLUSTER_COUNT_FAILURE_REPORTS from '../commands/CLUSTER_COUNT-FAILURE-REPORTS';
|
||||
import * as CLUSTER_COUNTKEYSINSLOT from '../commands/CLUSTER_COUNTKEYSINSLOT';
|
||||
import * as CLUSTER_DELSLOTS from '../commands/CLUSTER_DELSLOTS';
|
||||
import * as CLUSTER_DELSLOTSRANGE from '../commands/CLUSTER_DELSLOTSRANGE';
|
||||
import * as CLUSTER_FAILOVER from '../commands/CLUSTER_FAILOVER';
|
||||
import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS';
|
||||
import * as CLUSTER_FORGET from '../commands/CLUSTER_FORGET';
|
||||
import * as CLUSTER_GETKEYSINSLOT from '../commands/CLUSTER_GETKEYSINSLOT';
|
||||
import * as CLUSTER_INFO from '../commands/CLUSTER_INFO';
|
||||
import * as CLUSTER_KEYSLOT from '../commands/CLUSTER_KEYSLOT';
|
||||
import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS';
|
||||
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
|
||||
import * as CLUSTER_MYID from '../commands/CLUSTER_MYID';
|
||||
import * as CLUSTER_MYSHARDID from '../commands/CLUSTER_MYSHARDID';
|
||||
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
|
||||
import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS';
|
||||
import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE';
|
||||
import * as CLUSTER_RESET from '../commands/CLUSTER_RESET';
|
||||
import * as CLUSTER_SAVECONFIG from '../commands/CLUSTER_SAVECONFIG';
|
||||
import * as CLUSTER_SET_CONFIG_EPOCH from '../commands/CLUSTER_SET-CONFIG-EPOCH';
|
||||
import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT';
|
||||
import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS';
|
||||
import * as COMMAND_COUNT from '../commands/COMMAND_COUNT';
|
||||
import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS';
|
||||
import * as COMMAND_GETKEYSANDFLAGS from '../commands/COMMAND_GETKEYSANDFLAGS';
|
||||
import * as COMMAND_INFO from '../commands/COMMAND_INFO';
|
||||
import * as COMMAND_LIST from '../commands/COMMAND_LIST';
|
||||
import * as COMMAND from '../commands/COMMAND';
|
||||
import * as CONFIG_GET from '../commands/CONFIG_GET';
|
||||
import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT';
|
||||
import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE';
|
||||
import * as CONFIG_SET from '../commands/CONFIG_SET';
|
||||
import * as DBSIZE from '../commands/DBSIZE';
|
||||
import * as DISCARD from '../commands/DISCARD';
|
||||
import * as ECHO from '../commands/ECHO';
|
||||
import * as FAILOVER from '../commands/FAILOVER';
|
||||
import * as FLUSHALL from '../commands/FLUSHALL';
|
||||
import * as FLUSHDB from '../commands/FLUSHDB';
|
||||
import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE';
|
||||
import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP';
|
||||
import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH';
|
||||
import * as FUNCTION_KILL from '../commands/FUNCTION_KILL';
|
||||
import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE';
|
||||
import * as FUNCTION_LIST from '../commands/FUNCTION_LIST';
|
||||
import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD';
|
||||
import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE';
|
||||
import * as FUNCTION_STATS from '../commands/FUNCTION_STATS';
|
||||
import * as HELLO from '../commands/HELLO';
|
||||
import * as INFO from '../commands/INFO';
|
||||
import * as KEYS from '../commands/KEYS';
|
||||
import * as LASTSAVE from '../commands/LASTSAVE';
|
||||
import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR';
|
||||
import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH';
|
||||
import * as LATENCY_HISTORY from '../commands/LATENCY_HISTORY';
|
||||
import * as LATENCY_LATEST from '../commands/LATENCY_LATEST';
|
||||
import * as LOLWUT from '../commands/LOLWUT';
|
||||
import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR';
|
||||
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
|
||||
import * as MEMORY_PURGE from '../commands/MEMORY_PURGE';
|
||||
import * as MEMORY_STATS from '../commands/MEMORY_STATS';
|
||||
import * as MEMORY_USAGE from '../commands/MEMORY_USAGE';
|
||||
import * as MODULE_LIST from '../commands/MODULE_LIST';
|
||||
import * as MODULE_LOAD from '../commands/MODULE_LOAD';
|
||||
import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD';
|
||||
import * as MOVE from '../commands/MOVE';
|
||||
import * as PING from '../commands/PING';
|
||||
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
|
||||
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
|
||||
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
|
||||
import * as PUBSUB_SHARDCHANNELS from '../commands/PUBSUB_SHARDCHANNELS';
|
||||
import * as PUBSUB_SHARDNUMSUB from '../commands/PUBSUB_SHARDNUMSUB';
|
||||
import * as RANDOMKEY from '../commands/RANDOMKEY';
|
||||
import * as READONLY from '../commands/READONLY';
|
||||
import * as READWRITE from '../commands/READWRITE';
|
||||
import * as REPLICAOF from '../commands/REPLICAOF';
|
||||
import * as RESTORE_ASKING from '../commands/RESTORE-ASKING';
|
||||
import * as ROLE from '../commands/ROLE';
|
||||
import * as SAVE from '../commands/SAVE';
|
||||
import * as SCAN from '../commands/SCAN';
|
||||
import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG';
|
||||
import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS';
|
||||
import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH';
|
||||
import * as SCRIPT_KILL from '../commands/SCRIPT_KILL';
|
||||
import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD';
|
||||
import * as SHUTDOWN from '../commands/SHUTDOWN';
|
||||
import * as SWAPDB from '../commands/SWAPDB';
|
||||
import * as TIME from '../commands/TIME';
|
||||
import * as UNWATCH from '../commands/UNWATCH';
|
||||
import * as WAIT from '../commands/WAIT';
|
||||
|
||||
export default {
|
||||
...CLUSTER_COMMANDS,
|
||||
ACL_CAT,
|
||||
aclCat: ACL_CAT,
|
||||
ACL_DELUSER,
|
||||
aclDelUser: ACL_DELUSER,
|
||||
ACL_DRYRUN,
|
||||
aclDryRun: ACL_DRYRUN,
|
||||
ACL_GENPASS,
|
||||
aclGenPass: ACL_GENPASS,
|
||||
ACL_GETUSER,
|
||||
aclGetUser: ACL_GETUSER,
|
||||
ACL_LIST,
|
||||
aclList: ACL_LIST,
|
||||
ACL_LOAD,
|
||||
aclLoad: ACL_LOAD,
|
||||
ACL_LOG_RESET,
|
||||
aclLogReset: ACL_LOG_RESET,
|
||||
ACL_LOG,
|
||||
aclLog: ACL_LOG,
|
||||
ACL_SAVE,
|
||||
aclSave: ACL_SAVE,
|
||||
ACL_SETUSER,
|
||||
aclSetUser: ACL_SETUSER,
|
||||
ACL_USERS,
|
||||
aclUsers: ACL_USERS,
|
||||
ACL_WHOAMI,
|
||||
aclWhoAmI: ACL_WHOAMI,
|
||||
ASKING,
|
||||
asking: ASKING,
|
||||
AUTH,
|
||||
auth: AUTH,
|
||||
BGREWRITEAOF,
|
||||
bgRewriteAof: BGREWRITEAOF,
|
||||
BGSAVE,
|
||||
bgSave: BGSAVE,
|
||||
CLIENT_CACHING,
|
||||
clientCaching: CLIENT_CACHING,
|
||||
CLIENT_GETNAME,
|
||||
clientGetName: CLIENT_GETNAME,
|
||||
CLIENT_GETREDIR,
|
||||
clientGetRedir: CLIENT_GETREDIR,
|
||||
CLIENT_ID,
|
||||
clientId: CLIENT_ID,
|
||||
CLIENT_KILL,
|
||||
clientKill: CLIENT_KILL,
|
||||
'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
|
||||
clientNoEvict: CLIENT_NO_EVICT,
|
||||
'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH,
|
||||
clientNoTouch: CLIENT_NO_TOUCH,
|
||||
CLIENT_LIST,
|
||||
clientList: CLIENT_LIST,
|
||||
CLIENT_PAUSE,
|
||||
clientPause: CLIENT_PAUSE,
|
||||
CLIENT_SETNAME,
|
||||
clientSetName: CLIENT_SETNAME,
|
||||
CLIENT_TRACKING,
|
||||
clientTracking: CLIENT_TRACKING,
|
||||
CLIENT_TRACKINGINFO,
|
||||
clientTrackingInfo: CLIENT_TRACKINGINFO,
|
||||
CLIENT_UNPAUSE,
|
||||
clientUnpause: CLIENT_UNPAUSE,
|
||||
CLIENT_INFO,
|
||||
clientInfo: CLIENT_INFO,
|
||||
CLUSTER_ADDSLOTS,
|
||||
clusterAddSlots: CLUSTER_ADDSLOTS,
|
||||
CLUSTER_ADDSLOTSRANGE,
|
||||
clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE,
|
||||
CLUSTER_BUMPEPOCH,
|
||||
clusterBumpEpoch: CLUSTER_BUMPEPOCH,
|
||||
CLUSTER_COUNT_FAILURE_REPORTS,
|
||||
clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS,
|
||||
CLUSTER_COUNTKEYSINSLOT,
|
||||
clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT,
|
||||
CLUSTER_DELSLOTS,
|
||||
clusterDelSlots: CLUSTER_DELSLOTS,
|
||||
CLUSTER_DELSLOTSRANGE,
|
||||
clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE,
|
||||
CLUSTER_FAILOVER,
|
||||
clusterFailover: CLUSTER_FAILOVER,
|
||||
CLUSTER_FLUSHSLOTS,
|
||||
clusterFlushSlots: CLUSTER_FLUSHSLOTS,
|
||||
CLUSTER_FORGET,
|
||||
clusterForget: CLUSTER_FORGET,
|
||||
CLUSTER_GETKEYSINSLOT,
|
||||
clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
|
||||
CLUSTER_INFO,
|
||||
clusterInfo: CLUSTER_INFO,
|
||||
CLUSTER_KEYSLOT,
|
||||
clusterKeySlot: CLUSTER_KEYSLOT,
|
||||
CLUSTER_LINKS,
|
||||
clusterLinks: CLUSTER_LINKS,
|
||||
CLUSTER_MEET,
|
||||
clusterMeet: CLUSTER_MEET,
|
||||
CLUSTER_MYID,
|
||||
clusterMyId: CLUSTER_MYID,
|
||||
CLUSTER_MYSHARDID,
|
||||
clusterMyShardId: CLUSTER_MYSHARDID,
|
||||
CLUSTER_NODES,
|
||||
clusterNodes: CLUSTER_NODES,
|
||||
CLUSTER_REPLICAS,
|
||||
clusterReplicas: CLUSTER_REPLICAS,
|
||||
CLUSTER_REPLICATE,
|
||||
clusterReplicate: CLUSTER_REPLICATE,
|
||||
CLUSTER_RESET,
|
||||
clusterReset: CLUSTER_RESET,
|
||||
CLUSTER_SAVECONFIG,
|
||||
clusterSaveConfig: CLUSTER_SAVECONFIG,
|
||||
CLUSTER_SET_CONFIG_EPOCH,
|
||||
clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH,
|
||||
CLUSTER_SETSLOT,
|
||||
clusterSetSlot: CLUSTER_SETSLOT,
|
||||
CLUSTER_SLOTS,
|
||||
clusterSlots: CLUSTER_SLOTS,
|
||||
COMMAND_COUNT,
|
||||
commandCount: COMMAND_COUNT,
|
||||
COMMAND_GETKEYS,
|
||||
commandGetKeys: COMMAND_GETKEYS,
|
||||
COMMAND_GETKEYSANDFLAGS,
|
||||
commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS,
|
||||
COMMAND_INFO,
|
||||
commandInfo: COMMAND_INFO,
|
||||
COMMAND_LIST,
|
||||
commandList: COMMAND_LIST,
|
||||
COMMAND,
|
||||
command: COMMAND,
|
||||
CONFIG_GET,
|
||||
configGet: CONFIG_GET,
|
||||
CONFIG_RESETASTAT,
|
||||
configResetStat: CONFIG_RESETASTAT,
|
||||
CONFIG_REWRITE,
|
||||
configRewrite: CONFIG_REWRITE,
|
||||
CONFIG_SET,
|
||||
configSet: CONFIG_SET,
|
||||
DBSIZE,
|
||||
dbSize: DBSIZE,
|
||||
DISCARD,
|
||||
discard: DISCARD,
|
||||
ECHO,
|
||||
echo: ECHO,
|
||||
FAILOVER,
|
||||
failover: FAILOVER,
|
||||
FLUSHALL,
|
||||
flushAll: FLUSHALL,
|
||||
FLUSHDB,
|
||||
flushDb: FLUSHDB,
|
||||
FUNCTION_DELETE,
|
||||
functionDelete: FUNCTION_DELETE,
|
||||
FUNCTION_DUMP,
|
||||
functionDump: FUNCTION_DUMP,
|
||||
FUNCTION_FLUSH,
|
||||
functionFlush: FUNCTION_FLUSH,
|
||||
FUNCTION_KILL,
|
||||
functionKill: FUNCTION_KILL,
|
||||
FUNCTION_LIST_WITHCODE,
|
||||
functionListWithCode: FUNCTION_LIST_WITHCODE,
|
||||
FUNCTION_LIST,
|
||||
functionList: FUNCTION_LIST,
|
||||
FUNCTION_LOAD,
|
||||
functionLoad: FUNCTION_LOAD,
|
||||
FUNCTION_RESTORE,
|
||||
functionRestore: FUNCTION_RESTORE,
|
||||
FUNCTION_STATS,
|
||||
functionStats: FUNCTION_STATS,
|
||||
HELLO,
|
||||
hello: HELLO,
|
||||
INFO,
|
||||
info: INFO,
|
||||
KEYS,
|
||||
keys: KEYS,
|
||||
LASTSAVE,
|
||||
lastSave: LASTSAVE,
|
||||
LATENCY_DOCTOR,
|
||||
latencyDoctor: LATENCY_DOCTOR,
|
||||
LATENCY_GRAPH,
|
||||
latencyGraph: LATENCY_GRAPH,
|
||||
LATENCY_HISTORY,
|
||||
latencyHistory: LATENCY_HISTORY,
|
||||
LATENCY_LATEST,
|
||||
latencyLatest: LATENCY_LATEST,
|
||||
LOLWUT,
|
||||
lolwut: LOLWUT,
|
||||
MEMORY_DOCTOR,
|
||||
memoryDoctor: MEMORY_DOCTOR,
|
||||
'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
|
||||
memoryMallocStats: MEMORY_MALLOC_STATS,
|
||||
MEMORY_PURGE,
|
||||
memoryPurge: MEMORY_PURGE,
|
||||
MEMORY_STATS,
|
||||
memoryStats: MEMORY_STATS,
|
||||
MEMORY_USAGE,
|
||||
memoryUsage: MEMORY_USAGE,
|
||||
MODULE_LIST,
|
||||
moduleList: MODULE_LIST,
|
||||
MODULE_LOAD,
|
||||
moduleLoad: MODULE_LOAD,
|
||||
MODULE_UNLOAD,
|
||||
moduleUnload: MODULE_UNLOAD,
|
||||
MOVE,
|
||||
move: MOVE,
|
||||
PING,
|
||||
ping: PING,
|
||||
PUBSUB_CHANNELS,
|
||||
pubSubChannels: PUBSUB_CHANNELS,
|
||||
PUBSUB_NUMPAT,
|
||||
pubSubNumPat: PUBSUB_NUMPAT,
|
||||
PUBSUB_NUMSUB,
|
||||
pubSubNumSub: PUBSUB_NUMSUB,
|
||||
PUBSUB_SHARDCHANNELS,
|
||||
pubSubShardChannels: PUBSUB_SHARDCHANNELS,
|
||||
PUBSUB_SHARDNUMSUB,
|
||||
pubSubShardNumSub: PUBSUB_SHARDNUMSUB,
|
||||
RANDOMKEY,
|
||||
randomKey: RANDOMKEY,
|
||||
READONLY,
|
||||
readonly: READONLY,
|
||||
READWRITE,
|
||||
readwrite: READWRITE,
|
||||
REPLICAOF,
|
||||
replicaOf: REPLICAOF,
|
||||
'RESTORE-ASKING': RESTORE_ASKING,
|
||||
restoreAsking: RESTORE_ASKING,
|
||||
ROLE,
|
||||
role: ROLE,
|
||||
SAVE,
|
||||
save: SAVE,
|
||||
SCAN,
|
||||
scan: SCAN,
|
||||
SCRIPT_DEBUG,
|
||||
scriptDebug: SCRIPT_DEBUG,
|
||||
SCRIPT_EXISTS,
|
||||
scriptExists: SCRIPT_EXISTS,
|
||||
SCRIPT_FLUSH,
|
||||
scriptFlush: SCRIPT_FLUSH,
|
||||
SCRIPT_KILL,
|
||||
scriptKill: SCRIPT_KILL,
|
||||
SCRIPT_LOAD,
|
||||
scriptLoad: SCRIPT_LOAD,
|
||||
SHUTDOWN,
|
||||
shutdown: SHUTDOWN,
|
||||
SWAPDB,
|
||||
swapDb: SWAPDB,
|
||||
TIME,
|
||||
time: TIME,
|
||||
UNWATCH,
|
||||
unwatch: UNWATCH,
|
||||
WAIT,
|
||||
wait: WAIT
|
||||
};
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
111
packages/client/lib/client/legacy-mode.spec.ts
Normal file
111
packages/client/lib/client/legacy-mode.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { promisify } from 'node:util';
|
||||
import { RedisLegacyClientType } from './legacy-mode';
|
||||
import { ErrorReply } from '../errors';
|
||||
import { RedisClientType } from '.';
|
||||
import { once } from 'node:events';
|
||||
|
||||
function testWithLegacyClient(title: string, fn: (legacy: RedisLegacyClientType, client: RedisClientType) => Promise<unknown>) {
|
||||
testUtils.testWithClient(title, client => fn(client.legacy(), client), GLOBAL.SERVERS.OPEN);
|
||||
}
|
||||
|
||||
describe('Legacy Mode', () => {
|
||||
describe('client.sendCommand', () => {
|
||||
testWithLegacyClient('resolve', async client => {
|
||||
assert.equal(
|
||||
await promisify(client.sendCommand).call(client, 'PING'),
|
||||
'PONG'
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject', async client => {
|
||||
await assert.rejects(
|
||||
promisify(client.sendCommand).call(client, 'ERROR'),
|
||||
ErrorReply
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject without a callback', async (legacy, client) => {
|
||||
legacy.sendCommand('ERROR');
|
||||
const [err] = await once(client, 'error');
|
||||
assert.ok(err instanceof ErrorReply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hGetAll (TRANSFORM_LEGACY_REPLY)', () => {
|
||||
testWithLegacyClient('resolve', async client => {
|
||||
await promisify(client.hSet).call(client, 'key', 'field', 'value');
|
||||
assert.deepEqual(
|
||||
await promisify(client.hGetAll).call(client, 'key'),
|
||||
Object.create(null, {
|
||||
field: {
|
||||
value: 'value',
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject', async client => {
|
||||
await assert.rejects(
|
||||
promisify(client.hGetAll).call(client),
|
||||
ErrorReply
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('client.set', () => {
|
||||
testWithLegacyClient('vardict', async client => {
|
||||
assert.equal(
|
||||
await promisify(client.set).call(client, 'a', 'b'),
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('array', async client => {
|
||||
assert.equal(
|
||||
await promisify(client.set).call(client, ['a', 'b']),
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('vardict & arrays', async client => {
|
||||
assert.equal(
|
||||
await promisify(client.set).call(client, ['a'], 'b', ['EX', 1]),
|
||||
'OK'
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject without a callback', async (legacy, client) => {
|
||||
legacy.set('ERROR');
|
||||
const [err] = await once(client, 'error');
|
||||
assert.ok(err instanceof ErrorReply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('client.multi', () => {
|
||||
testWithLegacyClient('resolve', async client => {
|
||||
const multi = client.multi().ping().sendCommand('PING');
|
||||
assert.deepEqual(
|
||||
await promisify(multi.exec).call(multi),
|
||||
['PONG', 'PONG']
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject', async client => {
|
||||
const multi = client.multi().sendCommand('ERROR');
|
||||
await assert.rejects(
|
||||
promisify(multi.exec).call(multi),
|
||||
ErrorReply
|
||||
);
|
||||
});
|
||||
|
||||
testWithLegacyClient('reject without a callback', async (legacy, client) => {
|
||||
legacy.multi().sendCommand('ERROR').exec();
|
||||
const [err] = await once(client, 'error');
|
||||
assert.ok(err instanceof ErrorReply);
|
||||
});
|
||||
});
|
||||
});
|
177
packages/client/lib/client/legacy-mode.ts
Normal file
177
packages/client/lib/client/legacy-mode.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { RedisModules, RedisFunctions, RedisScripts, RespVersions, Command, CommandArguments, ReplyUnion } from '../RESP/types';
|
||||
import { RedisClientType } from '.';
|
||||
import { getTransformReply } from '../commander';
|
||||
import { ErrorReply } from '../errors';
|
||||
import COMMANDS from '../commands';
|
||||
import RedisMultiCommand from '../multi-command';
|
||||
|
||||
type LegacyArgument = string | Buffer | number | Date;
|
||||
|
||||
type LegacyArguments = Array<LegacyArgument | LegacyArguments>;
|
||||
|
||||
type LegacyCallback = (err: ErrorReply | null, reply?: ReplyUnion) => unknown
|
||||
|
||||
type LegacyCommandArguments = LegacyArguments | [
|
||||
...args: LegacyArguments,
|
||||
callback: LegacyCallback
|
||||
];
|
||||
|
||||
type WithCommands = {
|
||||
[P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => void;
|
||||
};
|
||||
|
||||
export type RedisLegacyClientType = RedisLegacyClient & WithCommands;
|
||||
|
||||
export class RedisLegacyClient {
|
||||
static #transformArguments(redisArgs: CommandArguments, args: LegacyCommandArguments) {
|
||||
let callback: LegacyCallback | undefined;
|
||||
if (typeof args[args.length - 1] === 'function') {
|
||||
callback = args.pop() as LegacyCallback;
|
||||
}
|
||||
|
||||
RedisLegacyClient.pushArguments(redisArgs, args as LegacyArguments);
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
static pushArguments(redisArgs: CommandArguments, args: LegacyArguments) {
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
const arg = args[i];
|
||||
if (Array.isArray(arg)) {
|
||||
RedisLegacyClient.pushArguments(redisArgs, arg);
|
||||
} else {
|
||||
redisArgs.push(
|
||||
typeof arg === 'number' || arg instanceof Date ?
|
||||
arg.toString() :
|
||||
arg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getTransformReply(command: Command, resp: RespVersions) {
|
||||
return command.TRANSFORM_LEGACY_REPLY ?
|
||||
getTransformReply(command, resp) :
|
||||
undefined;
|
||||
}
|
||||
|
||||
static #createCommand(name: string, command: Command, resp: RespVersions) {
|
||||
const transformReply = RedisLegacyClient.getTransformReply(command, resp);
|
||||
return function (this: RedisLegacyClient, ...args: LegacyCommandArguments) {
|
||||
const redisArgs = [name],
|
||||
callback = RedisLegacyClient.#transformArguments(redisArgs, args),
|
||||
promise = this.#client.sendCommand(redisArgs);
|
||||
|
||||
if (!callback) {
|
||||
promise.catch(err => this.#client.emit('error', err));
|
||||
return;
|
||||
}
|
||||
|
||||
promise
|
||||
.then(reply => callback(null, transformReply ? transformReply(reply) : reply))
|
||||
.catch(err => callback(err));
|
||||
};
|
||||
}
|
||||
|
||||
#client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
|
||||
#Multi: ReturnType<typeof LegacyMultiCommand['factory']>;
|
||||
|
||||
constructor(
|
||||
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>
|
||||
) {
|
||||
this.#client = client;
|
||||
|
||||
const RESP = client.options?.RESP ?? 2;
|
||||
for (const [name, command] of Object.entries(COMMANDS)) {
|
||||
// TODO: as any?
|
||||
(this as any)[name] = RedisLegacyClient.#createCommand(
|
||||
name,
|
||||
command,
|
||||
RESP
|
||||
);
|
||||
}
|
||||
|
||||
this.#Multi = LegacyMultiCommand.factory(RESP);
|
||||
}
|
||||
|
||||
sendCommand(...args: LegacyCommandArguments) {
|
||||
const redisArgs: CommandArguments = [],
|
||||
callback = RedisLegacyClient.#transformArguments(redisArgs, args),
|
||||
promise = this.#client.sendCommand(redisArgs);
|
||||
|
||||
if (!callback) {
|
||||
promise.catch(err => this.#client.emit('error', err));
|
||||
return;
|
||||
}
|
||||
|
||||
promise
|
||||
.then(reply => callback(null, reply))
|
||||
.catch(err => callback(err));
|
||||
}
|
||||
|
||||
multi() {
|
||||
return this.#Multi(this.#client);
|
||||
}
|
||||
}
|
||||
|
||||
type MultiWithCommands = {
|
||||
[P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => RedisLegacyMultiType;
|
||||
};
|
||||
|
||||
export type RedisLegacyMultiType = LegacyMultiCommand & MultiWithCommands;
|
||||
|
||||
class LegacyMultiCommand {
|
||||
static #createCommand(name: string, command: Command, resp: RespVersions) {
|
||||
const transformReply = RedisLegacyClient.getTransformReply(command, resp);
|
||||
return function (this: LegacyMultiCommand, ...args: LegacyArguments) {
|
||||
const redisArgs = [name];
|
||||
RedisLegacyClient.pushArguments(redisArgs, args);
|
||||
this.#multi.addCommand(redisArgs, transformReply);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
static factory(resp: RespVersions) {
|
||||
const Multi = class extends LegacyMultiCommand {};
|
||||
|
||||
for (const [name, command] of Object.entries(COMMANDS)) {
|
||||
// TODO: as any?
|
||||
(Multi as any).prototype[name] = LegacyMultiCommand.#createCommand(
|
||||
name,
|
||||
command,
|
||||
resp
|
||||
);
|
||||
}
|
||||
|
||||
return (client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>) => {
|
||||
return new Multi(client) as unknown as RedisLegacyMultiType;
|
||||
};
|
||||
}
|
||||
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;
|
||||
|
||||
constructor(client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
sendCommand(...args: LegacyArguments) {
|
||||
const redisArgs: CommandArguments = [];
|
||||
RedisLegacyClient.pushArguments(redisArgs, args);
|
||||
this.#multi.addCommand(redisArgs);
|
||||
return this;
|
||||
}
|
||||
|
||||
exec(cb?: (err: ErrorReply | null, replies?: Array<unknown>) => unknown) {
|
||||
const promise = this.#client._executeMulti(this.#multi.queue);
|
||||
|
||||
if (!cb) {
|
||||
promise.catch(err => this.#client.emit('error', err));
|
||||
return;
|
||||
}
|
||||
|
||||
promise
|
||||
.then(results => cb(null, this.#multi.transformReplies(results)))
|
||||
.catch(err => cb?.(err));
|
||||
}
|
||||
}
|
138
packages/client/lib/client/linked-list.spec.ts
Normal file
138
packages/client/lib/client/linked-list.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { SinglyLinkedList, DoublyLinkedList } from './linked-list';
|
||||
import { equal, deepEqual } from 'assert/strict';
|
||||
|
||||
describe('DoublyLinkedList', () => {
|
||||
const list = new DoublyLinkedList();
|
||||
|
||||
it('should start empty', () => {
|
||||
equal(list.length, 0);
|
||||
equal(list.head, undefined);
|
||||
equal(list.tail, undefined);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('shift empty', () => {
|
||||
equal(list.shift(), undefined);
|
||||
equal(list.length, 0);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('push 1', () => {
|
||||
list.push(1);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [1]);
|
||||
});
|
||||
|
||||
it('push 2', () => {
|
||||
list.push(2);
|
||||
equal(list.length, 2);
|
||||
deepEqual(Array.from(list), [1, 2]);
|
||||
});
|
||||
|
||||
it('unshift 0', () => {
|
||||
list.unshift(0);
|
||||
equal(list.length, 3);
|
||||
deepEqual(Array.from(list), [0, 1, 2]);
|
||||
});
|
||||
|
||||
it('remove middle node', () => {
|
||||
list.remove(list.head!.next!);
|
||||
equal(list.length, 2);
|
||||
deepEqual(Array.from(list), [0, 2]);
|
||||
});
|
||||
|
||||
it('remove head', () => {
|
||||
list.remove(list.head!);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [2]);
|
||||
});
|
||||
|
||||
it('remove tail', () => {
|
||||
list.remove(list.tail!);
|
||||
equal(list.length, 0);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('unshift empty queue', () => {
|
||||
list.unshift(0);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [0]);
|
||||
});
|
||||
|
||||
it('push 1', () => {
|
||||
list.push(1);
|
||||
equal(list.length, 2);
|
||||
deepEqual(Array.from(list), [0, 1]);
|
||||
});
|
||||
|
||||
it('shift', () => {
|
||||
equal(list.shift(), 0);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [1]);
|
||||
});
|
||||
|
||||
it('shift last element', () => {
|
||||
equal(list.shift(), 1);
|
||||
equal(list.length, 0);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SinglyLinkedList', () => {
|
||||
const list = new SinglyLinkedList();
|
||||
|
||||
it('should start empty', () => {
|
||||
equal(list.length, 0);
|
||||
equal(list.head, undefined);
|
||||
equal(list.tail, undefined);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('shift empty', () => {
|
||||
equal(list.shift(), undefined);
|
||||
equal(list.length, 0);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('push 1', () => {
|
||||
list.push(1);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [1]);
|
||||
});
|
||||
|
||||
it('push 2', () => {
|
||||
list.push(2);
|
||||
equal(list.length, 2);
|
||||
deepEqual(Array.from(list), [1, 2]);
|
||||
});
|
||||
|
||||
it('push 3', () => {
|
||||
list.push(3);
|
||||
equal(list.length, 3);
|
||||
deepEqual(Array.from(list), [1, 2, 3]);
|
||||
});
|
||||
|
||||
it('shift 1', () => {
|
||||
equal(list.shift(), 1);
|
||||
equal(list.length, 2);
|
||||
deepEqual(Array.from(list), [2, 3]);
|
||||
});
|
||||
|
||||
it('shift 2', () => {
|
||||
equal(list.shift(), 2);
|
||||
equal(list.length, 1);
|
||||
deepEqual(Array.from(list), [3]);
|
||||
});
|
||||
|
||||
it('shift 3', () => {
|
||||
equal(list.shift(), 3);
|
||||
equal(list.length, 0);
|
||||
deepEqual(Array.from(list), []);
|
||||
});
|
||||
|
||||
it('should be empty', () => {
|
||||
equal(list.length, 0);
|
||||
equal(list.head, undefined);
|
||||
equal(list.tail, undefined);
|
||||
});
|
||||
});
|
195
packages/client/lib/client/linked-list.ts
Normal file
195
packages/client/lib/client/linked-list.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
export interface DoublyLinkedNode<T> {
|
||||
value: T;
|
||||
previous: DoublyLinkedNode<T> | undefined;
|
||||
next: DoublyLinkedNode<T> | undefined;
|
||||
}
|
||||
|
||||
export class DoublyLinkedList<T> {
|
||||
#length = 0;
|
||||
|
||||
get length() {
|
||||
return this.#length;
|
||||
}
|
||||
|
||||
#head?: DoublyLinkedNode<T>;
|
||||
|
||||
get head() {
|
||||
return this.#head;
|
||||
}
|
||||
|
||||
#tail?: DoublyLinkedNode<T>;
|
||||
|
||||
get tail() {
|
||||
return this.#tail;
|
||||
}
|
||||
|
||||
push(value: T) {
|
||||
++this.#length;
|
||||
|
||||
if (this.#tail === undefined) {
|
||||
return this.#tail = this.#head = {
|
||||
previous: this.#head,
|
||||
next: undefined,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
return this.#tail = this.#tail.next = {
|
||||
previous: this.#tail,
|
||||
next: undefined,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
unshift(value: T) {
|
||||
++this.#length;
|
||||
|
||||
if (this.#head === undefined) {
|
||||
return this.#head = this.#tail = {
|
||||
previous: undefined,
|
||||
next: undefined,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
return this.#head = this.#head.previous = {
|
||||
previous: undefined,
|
||||
next: this.#head,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
add(value: T, prepend = false) {
|
||||
return prepend ?
|
||||
this.unshift(value) :
|
||||
this.push(value);
|
||||
}
|
||||
|
||||
shift() {
|
||||
if (this.#head === undefined) return undefined;
|
||||
|
||||
--this.#length;
|
||||
const node = this.#head;
|
||||
if (node.next) {
|
||||
node.next.previous = node.previous;
|
||||
this.#head = node.next;
|
||||
node.next = undefined;
|
||||
} else {
|
||||
this.#head = this.#tail = undefined;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
remove(node: DoublyLinkedNode<T>) {
|
||||
--this.#length;
|
||||
|
||||
if (this.#tail === node) {
|
||||
this.#tail = node.previous;
|
||||
}
|
||||
|
||||
if (this.#head === node) {
|
||||
this.#head = node.next;
|
||||
} else {
|
||||
node.previous!.next = node.next;
|
||||
node.previous = undefined;
|
||||
}
|
||||
|
||||
node.next = undefined;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#length = 0;
|
||||
this.#head = this.#tail = undefined;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
let node = this.#head;
|
||||
while (node !== undefined) {
|
||||
yield node.value;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SinglyLinkedNode<T> {
|
||||
value: T;
|
||||
next: SinglyLinkedNode<T> | undefined;
|
||||
}
|
||||
|
||||
export class SinglyLinkedList<T> {
|
||||
#length = 0;
|
||||
|
||||
get length() {
|
||||
return this.#length;
|
||||
}
|
||||
|
||||
#head?: SinglyLinkedNode<T>;
|
||||
|
||||
get head() {
|
||||
return this.#head;
|
||||
}
|
||||
|
||||
#tail?: SinglyLinkedNode<T>;
|
||||
|
||||
get tail() {
|
||||
return this.#tail;
|
||||
}
|
||||
|
||||
push(value: T) {
|
||||
++this.#length;
|
||||
|
||||
const node = {
|
||||
value,
|
||||
next: undefined
|
||||
};
|
||||
|
||||
if (this.#head === undefined) {
|
||||
return this.#head = this.#tail = node;
|
||||
}
|
||||
|
||||
return this.#tail!.next = this.#tail = node;
|
||||
}
|
||||
|
||||
remove(node: SinglyLinkedNode<T>, parent: SinglyLinkedNode<T> | undefined) {
|
||||
--this.#length;
|
||||
|
||||
if (this.#head === node) {
|
||||
if (this.#tail === node) {
|
||||
this.#head = this.#tail = undefined;
|
||||
} else {
|
||||
this.#head = node.next;
|
||||
}
|
||||
} else if (this.#tail === node) {
|
||||
this.#tail = parent;
|
||||
parent!.next = undefined;
|
||||
} else {
|
||||
parent!.next = node.next;
|
||||
}
|
||||
}
|
||||
|
||||
shift() {
|
||||
if (this.#head === undefined) return undefined;
|
||||
|
||||
const node = this.#head;
|
||||
if (--this.#length === 0) {
|
||||
this.#head = this.#tail = undefined;
|
||||
} else {
|
||||
this.#head = node.next;
|
||||
}
|
||||
|
||||
return node.value;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#length = 0;
|
||||
this.#head = this.#tail = undefined;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
let node = this.#head;
|
||||
while (node !== undefined) {
|
||||
yield node.value;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,200 +1,205 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction, RedisCommands } from '../commands';
|
||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander';
|
||||
import COMMANDS from '../commands';
|
||||
import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types';
|
||||
import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
|
||||
|
||||
type CommandSignature<
|
||||
C extends RedisCommand,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, F, S>;
|
||||
REPLIES extends Array<unknown>,
|
||||
C extends Command,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<
|
||||
[...REPLIES, ReplyWithTypeMapping<CommandReply<C, RESP>, TYPE_MAPPING>],
|
||||
M,
|
||||
F,
|
||||
S,
|
||||
RESP,
|
||||
TYPE_MAPPING
|
||||
>;
|
||||
|
||||
type WithCommands<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>;
|
||||
[P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
|
||||
type WithModules<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof M as ExcludeMappedString<P>]: {
|
||||
[C in keyof M[P] as ExcludeMappedString<C>]: CommandSignature<M[P][C], M, F, S>;
|
||||
};
|
||||
[P in keyof M]: {
|
||||
[C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithFunctions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof F as ExcludeMappedString<P>]: {
|
||||
[FF in keyof F[P] as ExcludeMappedString<FF>]: CommandSignature<F[P][FF], M, F, S>;
|
||||
};
|
||||
[L in keyof F]: {
|
||||
[C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof S as ExcludeMappedString<P>]: CommandSignature<S[P], M, F, S>;
|
||||
[P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
|
||||
export type RedisClientMultiCommandType<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = RedisClientMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
|
||||
REPLIES extends Array<any>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (
|
||||
RedisClientMultiCommand<REPLIES> &
|
||||
WithCommands<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithModules<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithFunctions<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithScripts<REPLIES, M, F, S, RESP, TYPE_MAPPING>
|
||||
);
|
||||
|
||||
type InstantiableRedisMultiCommand<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<M, F, S>;
|
||||
type ExecuteMulti = (commands: Array<RedisMultiQueuedCommand>, selectedDB?: number) => Promise<Array<unknown>>;
|
||||
|
||||
export type RedisClientMultiExecutor = (
|
||||
queue: Array<RedisMultiQueuedCommand>,
|
||||
selectedDB?: number,
|
||||
chainId?: symbol
|
||||
) => Promise<Array<RedisCommandRawReply>>;
|
||||
export default class RedisClientMultiCommand<REPLIES = []> {
|
||||
static #createCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return function (this: RedisClientMultiCommand, ...args: Array<unknown>) {
|
||||
return this.addCommand(
|
||||
command.transformArguments(...args),
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default class RedisClientMultiCommand {
|
||||
static extend<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisMultiCommand<M, F, S> {
|
||||
return attachExtensions({
|
||||
BaseClass: RedisClientMultiCommand,
|
||||
modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
|
||||
modules: extensions?.modules,
|
||||
functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor,
|
||||
functions: extensions?.functions,
|
||||
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor,
|
||||
scripts: extensions?.scripts
|
||||
});
|
||||
}
|
||||
static #createModuleCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return function (this: { _self: RedisClientMultiCommand }, ...args: Array<unknown>) {
|
||||
return this._self.addCommand(
|
||||
command.transformArguments(...args),
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executor: RedisClientMultiExecutor;
|
||||
readonly v4: Record<string, any> = {};
|
||||
#selectedDB?: number;
|
||||
static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
|
||||
const prefix = functionArgumentsPrefix(name, fn),
|
||||
transformReply = getTransformReply(fn, resp);
|
||||
return function (this: { _self: RedisClientMultiCommand }, ...args: Array<unknown>) {
|
||||
const fnArgs = fn.transformArguments(...args),
|
||||
redisArgs: CommandArguments = prefix.concat(fnArgs);
|
||||
redisArgs.preserve = fnArgs.preserve;
|
||||
return this._self.addCommand(
|
||||
redisArgs,
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
constructor(executor: RedisClientMultiExecutor, legacyMode = false) {
|
||||
this.#executor = executor;
|
||||
if (legacyMode) {
|
||||
this.#legacyMode();
|
||||
}
|
||||
}
|
||||
static #createScriptCommand(script: RedisScript, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(script, resp);
|
||||
return function (this: RedisClientMultiCommand, ...args: Array<unknown>) {
|
||||
this.#multi.addScript(
|
||||
script,
|
||||
script.transformArguments(...args),
|
||||
transformReply
|
||||
);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
#legacyMode(): void {
|
||||
this.v4.addCommand = this.addCommand.bind(this);
|
||||
(this as any).addCommand = (...args: Array<any>): this => {
|
||||
this.#multi.addCommand(transformLegacyCommandArguments(args));
|
||||
return this;
|
||||
};
|
||||
this.v4.exec = this.exec.bind(this);
|
||||
(this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
|
||||
this.v4.exec()
|
||||
.then((reply: Array<unknown>) => {
|
||||
if (!callback) return;
|
||||
static extend<
|
||||
M extends RedisModules = Record<string, never>,
|
||||
F extends RedisFunctions = Record<string, never>,
|
||||
S extends RedisScripts = Record<string, never>,
|
||||
RESP extends RespVersions = 2
|
||||
>(config?: CommanderConfig<M, F, S, RESP>) {
|
||||
return attachConfig({
|
||||
BaseClass: RedisClientMultiCommand,
|
||||
commands: COMMANDS,
|
||||
createCommand: RedisClientMultiCommand.#createCommand,
|
||||
createModuleCommand: RedisClientMultiCommand.#createModuleCommand,
|
||||
createFunctionCommand: RedisClientMultiCommand.#createFunctionCommand,
|
||||
createScriptCommand: RedisClientMultiCommand.#createScriptCommand,
|
||||
config
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, reply);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (!callback) {
|
||||
// this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executeMulti: ExecuteMulti;
|
||||
readonly #executePipeline: ExecuteMulti;
|
||||
readonly #typeMapping?: TypeMapping;
|
||||
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
#selectedDB?: number;
|
||||
|
||||
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
|
||||
this.#defineLegacyCommand(name, command);
|
||||
(this as any)[name.toLowerCase()] ??= (this as any)[name];
|
||||
}
|
||||
}
|
||||
constructor(executeMulti: ExecuteMulti, executePipeline: ExecuteMulti, typeMapping?: TypeMapping) {
|
||||
this.#executeMulti = executeMulti;
|
||||
this.#executePipeline = executePipeline;
|
||||
this.#typeMapping = typeMapping;
|
||||
}
|
||||
|
||||
#defineLegacyCommand(this: any, name: string, command?: RedisCommand): void {
|
||||
this.v4[name] = this[name].bind(this.v4);
|
||||
this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
|
||||
(...args: Array<unknown>) => {
|
||||
this.#multi.addCommand(
|
||||
[name, ...transformLegacyCommandArguments(args)],
|
||||
command.transformReply
|
||||
);
|
||||
return this;
|
||||
} :
|
||||
(...args: Array<unknown>) => this.addCommand(name, ...args);
|
||||
}
|
||||
SELECT(db: number, transformReply?: TransformReply): this {
|
||||
this.#selectedDB = db;
|
||||
this.#multi.addCommand(['SELECT', db.toString()], transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
||||
return this.addCommand(
|
||||
command.transformArguments(...args),
|
||||
command.transformReply
|
||||
);
|
||||
}
|
||||
select = this.SELECT;
|
||||
|
||||
SELECT(db: number, transformReply?: RedisCommand['transformReply']): this {
|
||||
this.#selectedDB = db;
|
||||
return this.addCommand(['SELECT', db.toString()], transformReply);
|
||||
}
|
||||
addCommand(args: CommandArguments, transformReply?: TransformReply) {
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
select = this.SELECT;
|
||||
async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false): Promise<MultiReplyType<T, REPLIES>> {
|
||||
if (execAsPipeline) return this.execAsPipeline<T>();
|
||||
|
||||
addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this {
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executeMulti(this.#multi.queue, this.#selectedDB),
|
||||
this.#typeMapping
|
||||
) as MultiReplyType<T, REPLIES>;
|
||||
}
|
||||
|
||||
functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
|
||||
this.#multi.addFunction(name, fn, args);
|
||||
return this;
|
||||
}
|
||||
EXEC = this.exec;
|
||||
|
||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||
this.#multi.addScript(script, args);
|
||||
return this;
|
||||
}
|
||||
execTyped(execAsPipeline = false) {
|
||||
return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
|
||||
}
|
||||
|
||||
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
||||
if (execAsPipeline) {
|
||||
return this.execAsPipeline();
|
||||
}
|
||||
async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>(): Promise<MultiReplyType<T, REPLIES>> {
|
||||
if (this.#multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
|
||||
|
||||
return this.#multi.handleExecReplies(
|
||||
await this.#executor(
|
||||
this.#multi.queue,
|
||||
this.#selectedDB,
|
||||
RedisMultiCommand.generateChainId()
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executePipeline(this.#multi.queue, this.#selectedDB),
|
||||
this.#typeMapping
|
||||
) as MultiReplyType<T, REPLIES>;
|
||||
}
|
||||
|
||||
EXEC = this.exec;
|
||||
|
||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||
if (this.#multi.queue.length === 0) return [];
|
||||
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executor(
|
||||
this.#multi.queue,
|
||||
this.#selectedDB
|
||||
)
|
||||
);
|
||||
}
|
||||
execAsPipelineTyped() {
|
||||
return this.execAsPipeline<MULTI_REPLY['TYPED']>();
|
||||
}
|
||||
}
|
||||
|
||||
attachCommands({
|
||||
BaseClass: RedisClientMultiCommand,
|
||||
commands: COMMANDS,
|
||||
executor: RedisClientMultiCommand.prototype.commandsExecutor
|
||||
});
|
||||
|
11
packages/client/lib/client/pool.spec.ts
Normal file
11
packages/client/lib/client/pool.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
|
||||
describe('RedisClientPool', () => {
|
||||
testUtils.testWithClientPool('sendCommand', async pool => {
|
||||
assert.equal(
|
||||
await pool.sendCommand(['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
489
packages/client/lib/client/pool.ts
Normal file
489
packages/client/lib/client/pool.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import COMMANDS from '../commands';
|
||||
import { Command, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TypeMapping } from '../RESP/types';
|
||||
import RedisClient, { RedisClientType, RedisClientOptions, RedisClientExtensions } from '.';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { DoublyLinkedNode, DoublyLinkedList, SinglyLinkedList } from './linked-list';
|
||||
import { TimeoutError } from '../errors';
|
||||
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
|
||||
import { CommandOptions } from './commands-queue';
|
||||
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
||||
|
||||
export interface RedisPoolOptions {
|
||||
/**
|
||||
* The minimum number of clients to keep in the pool (>= 1).
|
||||
*/
|
||||
minimum: number;
|
||||
/**
|
||||
* The maximum number of clients to keep in the pool (>= {@link RedisPoolOptions.minimum} >= 1).
|
||||
*/
|
||||
maximum: number;
|
||||
/**
|
||||
* The maximum time a task can wait for a client to become available (>= 0).
|
||||
*/
|
||||
acquireTimeout: number;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
cleanupDelay: number;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
unstableResp3Modules?: boolean;
|
||||
}
|
||||
|
||||
export type PoolTask<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping,
|
||||
T = unknown
|
||||
> = (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => T;
|
||||
|
||||
export type RedisClientPoolType<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
> = (
|
||||
RedisClientPool<M, F, S, RESP, TYPE_MAPPING> &
|
||||
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
|
||||
);
|
||||
|
||||
type ProxyPool = RedisClientPoolType<any, any, any, any, any>;
|
||||
|
||||
type NamespaceProxyPool = { _self: ProxyPool };
|
||||
|
||||
export class RedisClientPool<
|
||||
M extends RedisModules = {},
|
||||
F extends RedisFunctions = {},
|
||||
S extends RedisScripts = {},
|
||||
RESP extends RespVersions = 2,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
> extends EventEmitter {
|
||||
static #createCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return async function (this: ProxyPool, ...args: Array<unknown>) {
|
||||
const redisArgs = command.transformArguments(...args);
|
||||
const typeMapping = this._commandOptions?.typeMapping;
|
||||
|
||||
const reply = await this.sendCommand(redisArgs, this._commandOptions);
|
||||
|
||||
return transformReply ?
|
||||
transformReply(reply, redisArgs.preserve, typeMapping) :
|
||||
reply;
|
||||
};
|
||||
}
|
||||
|
||||
static #createModuleCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
|
||||
const redisArgs = command.transformArguments(...args);
|
||||
const typeMapping = this._self._commandOptions?.typeMapping;
|
||||
|
||||
const reply = await this._self.sendCommand(redisArgs, this._self._commandOptions);
|
||||
|
||||
return transformReply ?
|
||||
transformReply(reply, redisArgs.preserve, typeMapping) :
|
||||
reply;
|
||||
};
|
||||
}
|
||||
|
||||
static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
|
||||
const prefix = functionArgumentsPrefix(name, fn),
|
||||
transformReply = getTransformReply(fn, resp);
|
||||
return async function (this: NamespaceProxyPool, ...args: Array<unknown>) {
|
||||
const fnArgs = fn.transformArguments(...args);
|
||||
const typeMapping = this._self._commandOptions?.typeMapping;
|
||||
|
||||
const reply = await this._self.sendCommand(
|
||||
prefix.concat(fnArgs),
|
||||
this._self._commandOptions
|
||||
);
|
||||
|
||||
return transformReply ?
|
||||
transformReply(reply, fnArgs.preserve, typeMapping) :
|
||||
reply;
|
||||
};
|
||||
}
|
||||
|
||||
static #createScriptCommand(script: RedisScript, resp: RespVersions) {
|
||||
const prefix = scriptArgumentsPrefix(script),
|
||||
transformReply = getTransformReply(script, resp);
|
||||
return async function (this: ProxyPool, ...args: Array<unknown>) {
|
||||
const scriptArgs = script.transformArguments(...args);
|
||||
const redisArgs = prefix.concat(scriptArgs);
|
||||
const typeMapping = this._commandOptions?.typeMapping;
|
||||
|
||||
const reply = await this.executeScript(script, redisArgs, this._commandOptions);
|
||||
|
||||
return transformReply ?
|
||||
transformReply(reply, scriptArgs.preserve, typeMapping) :
|
||||
reply;
|
||||
};
|
||||
}
|
||||
|
||||
static create<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping = {}
|
||||
>(
|
||||
clientOptions?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>,
|
||||
options?: Partial<RedisPoolOptions>
|
||||
) {
|
||||
const Pool = attachConfig({
|
||||
BaseClass: RedisClientPool,
|
||||
commands: COMMANDS,
|
||||
createCommand: RedisClientPool.#createCommand,
|
||||
createModuleCommand: RedisClientPool.#createModuleCommand,
|
||||
createFunctionCommand: RedisClientPool.#createFunctionCommand,
|
||||
createScriptCommand: RedisClientPool.#createScriptCommand,
|
||||
config: clientOptions
|
||||
});
|
||||
|
||||
Pool.prototype.Multi = RedisClientMultiCommand.extend(clientOptions);
|
||||
|
||||
// returning a "proxy" to prevent the namespaces._self to leak between "proxies"
|
||||
return Object.create(
|
||||
new Pool(
|
||||
RedisClient.factory(clientOptions).bind(undefined, clientOptions),
|
||||
options
|
||||
)
|
||||
) as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
|
||||
}
|
||||
|
||||
// TODO: defaults
|
||||
static #DEFAULTS = {
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
acquireTimeout: 3000,
|
||||
cleanupDelay: 3000
|
||||
} satisfies RedisPoolOptions;
|
||||
|
||||
readonly #clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>;
|
||||
readonly #options: RedisPoolOptions;
|
||||
|
||||
readonly #idleClients = new SinglyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
|
||||
|
||||
/**
|
||||
* The number of idle clients.
|
||||
*/
|
||||
get idleClients() {
|
||||
return this._self.#idleClients.length;
|
||||
}
|
||||
|
||||
readonly #clientsInUse = new DoublyLinkedList<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>();
|
||||
|
||||
/**
|
||||
* The number of clients in use.
|
||||
*/
|
||||
get clientsInUse() {
|
||||
return this._self.#clientsInUse.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total number of clients in the pool (including connecting, idle, and in use).
|
||||
*/
|
||||
get totalClients() {
|
||||
return this._self.#idleClients.length + this._self.#clientsInUse.length;
|
||||
}
|
||||
|
||||
readonly #tasksQueue = new SinglyLinkedList<{
|
||||
timeout: NodeJS.Timeout | undefined;
|
||||
resolve: (value: unknown) => unknown;
|
||||
reject: (reason?: unknown) => unknown;
|
||||
fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* The number of tasks waiting for a client to become available.
|
||||
*/
|
||||
get tasksQueueLength() {
|
||||
return this._self.#tasksQueue.length;
|
||||
}
|
||||
|
||||
#isOpen = false;
|
||||
|
||||
/**
|
||||
* Whether the pool is open (either connecting or connected).
|
||||
*/
|
||||
get isOpen() {
|
||||
return this._self.#isOpen;
|
||||
}
|
||||
|
||||
#isClosing = false;
|
||||
|
||||
/**
|
||||
* Whether the pool is closing (*not* closed).
|
||||
*/
|
||||
get isClosing() {
|
||||
return this._self.#isClosing;
|
||||
}
|
||||
|
||||
/**
|
||||
* You are probably looking for {@link RedisClient.createPool `RedisClient.createPool`},
|
||||
* {@link RedisClientPool.fromClient `RedisClientPool.fromClient`},
|
||||
* or {@link RedisClientPool.fromOptions `RedisClientPool.fromOptions`}...
|
||||
*/
|
||||
constructor(
|
||||
clientFactory: () => RedisClientType<M, F, S, RESP, TYPE_MAPPING>,
|
||||
options?: Partial<RedisPoolOptions>
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#clientFactory = clientFactory;
|
||||
this.#options = {
|
||||
...RedisClientPool.#DEFAULTS,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
private _self = this;
|
||||
private _commandOptions?: CommandOptions<TYPE_MAPPING>;
|
||||
|
||||
withCommandOptions<
|
||||
OPTIONS extends CommandOptions<TYPE_MAPPING>,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
>(options: OPTIONS) {
|
||||
const proxy = Object.create(this._self);
|
||||
proxy._commandOptions = options;
|
||||
return proxy as RedisClientPoolType<
|
||||
M,
|
||||
F,
|
||||
S,
|
||||
RESP,
|
||||
TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
|
||||
>;
|
||||
}
|
||||
|
||||
#commandOptionsProxy<
|
||||
K extends keyof CommandOptions,
|
||||
V extends CommandOptions[K]
|
||||
>(
|
||||
key: K,
|
||||
value: V
|
||||
) {
|
||||
const proxy = Object.create(this._self);
|
||||
proxy._commandOptions = Object.create(this._commandOptions ?? null);
|
||||
proxy._commandOptions[key] = value;
|
||||
return proxy as RedisClientPoolType<
|
||||
M,
|
||||
F,
|
||||
S,
|
||||
RESP,
|
||||
K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the `typeMapping` command option
|
||||
*/
|
||||
withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
|
||||
return this._self.#commandOptionsProxy('typeMapping', typeMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the `abortSignal` command option
|
||||
*/
|
||||
withAbortSignal(abortSignal: AbortSignal) {
|
||||
return this._self.#commandOptionsProxy('abortSignal', abortSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the `asap` command option to `true`
|
||||
* TODO: remove?
|
||||
*/
|
||||
asap() {
|
||||
return this._self.#commandOptionsProxy('asap', true);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this._self.#isOpen) return; // TODO: throw error?
|
||||
|
||||
this._self.#isOpen = true;
|
||||
|
||||
const promises = [];
|
||||
while (promises.length < this._self.#options.minimum) {
|
||||
promises.push(this._self.#create());
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
return this as unknown as RedisClientPoolType<M, F, S, RESP, TYPE_MAPPING>;
|
||||
} catch (err) {
|
||||
this.destroy();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async #create() {
|
||||
const node = this._self.#clientsInUse.push(
|
||||
this._self.#clientFactory()
|
||||
.on('error', (err: Error) => this.emit('error', err))
|
||||
);
|
||||
|
||||
try {
|
||||
await node.value.connect();
|
||||
} catch (err) {
|
||||
this._self.#clientsInUse.remove(node);
|
||||
throw err;
|
||||
}
|
||||
|
||||
this._self.#returnClient(node);
|
||||
}
|
||||
|
||||
execute<T>(fn: PoolTask<M, F, S, RESP, TYPE_MAPPING, T>) {
|
||||
return new Promise<Awaited<T>>((resolve, reject) => {
|
||||
const client = this._self.#idleClients.shift(),
|
||||
{ tail } = this._self.#tasksQueue;
|
||||
if (!client) {
|
||||
let timeout;
|
||||
if (this._self.#options.acquireTimeout > 0) {
|
||||
timeout = setTimeout(
|
||||
() => {
|
||||
this._self.#tasksQueue.remove(task, tail);
|
||||
reject(new TimeoutError('Timeout waiting for a client')); // TODO: message
|
||||
},
|
||||
this._self.#options.acquireTimeout
|
||||
);
|
||||
}
|
||||
|
||||
const task = this._self.#tasksQueue.push({
|
||||
timeout,
|
||||
// @ts-ignore
|
||||
resolve,
|
||||
reject,
|
||||
fn
|
||||
});
|
||||
|
||||
if (this.totalClients < this._self.#options.maximum) {
|
||||
this._self.#create();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const node = this._self.#clientsInUse.push(client);
|
||||
// @ts-ignore
|
||||
this._self.#executeTask(node, resolve, reject, fn);
|
||||
});
|
||||
}
|
||||
|
||||
#executeTask(
|
||||
node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>,
|
||||
resolve: <T>(value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
fn: PoolTask<M, F, S, RESP, TYPE_MAPPING>
|
||||
) {
|
||||
const result = fn(node.value);
|
||||
if (result instanceof Promise) {
|
||||
result.then(resolve, reject);
|
||||
result.finally(() => this.#returnClient(node))
|
||||
} else {
|
||||
resolve(result);
|
||||
this.#returnClient(node);
|
||||
}
|
||||
}
|
||||
|
||||
#returnClient(node: DoublyLinkedNode<RedisClientType<M, F, S, RESP, TYPE_MAPPING>>) {
|
||||
const task = this.#tasksQueue.shift();
|
||||
if (task) {
|
||||
clearTimeout(task.timeout);
|
||||
this.#executeTask(node, task.resolve, task.reject, task.fn);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#clientsInUse.remove(node);
|
||||
this.#idleClients.push(node.value);
|
||||
|
||||
this.#scheduleCleanup();
|
||||
}
|
||||
|
||||
cleanupTimeout?: NodeJS.Timeout;
|
||||
|
||||
#scheduleCleanup() {
|
||||
if (this.totalClients <= this.#options.minimum) return;
|
||||
|
||||
clearTimeout(this.cleanupTimeout);
|
||||
this.cleanupTimeout = setTimeout(() => this.#cleanup(), this.#options.cleanupDelay);
|
||||
}
|
||||
|
||||
#cleanup() {
|
||||
const toDestroy = Math.min(this.#idleClients.length, this.totalClients - this.#options.minimum);
|
||||
for (let i = 0; i < toDestroy; i++) {
|
||||
// TODO: shift vs pop
|
||||
this.#idleClients.shift()!.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
sendCommand(
|
||||
args: Array<RedisArgument>,
|
||||
options?: CommandOptions
|
||||
) {
|
||||
return this.execute(client => client.sendCommand(args, options));
|
||||
}
|
||||
|
||||
executeScript(
|
||||
script: RedisScript,
|
||||
args: Array<RedisArgument>,
|
||||
options?: CommandOptions
|
||||
) {
|
||||
return this.execute(client => client.executeScript(script, args, options));
|
||||
}
|
||||
|
||||
MULTI() {
|
||||
type Multi = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>;
|
||||
return new ((this as any).Multi as Multi)(
|
||||
(commands, selectedDB) => this.execute(client => client._executeMulti(commands, selectedDB)),
|
||||
commands => this.execute(client => client._executePipeline(commands)),
|
||||
this._commandOptions?.typeMapping
|
||||
);
|
||||
}
|
||||
|
||||
multi = this.MULTI;
|
||||
|
||||
async close() {
|
||||
if (this._self.#isClosing) return; // TODO: throw err?
|
||||
if (!this._self.#isOpen) return; // TODO: throw err?
|
||||
|
||||
this._self.#isClosing = true;
|
||||
|
||||
try {
|
||||
const promises = [];
|
||||
|
||||
for (const client of this._self.#idleClients) {
|
||||
promises.push(client.close());
|
||||
}
|
||||
|
||||
for (const client of this._self.#clientsInUse) {
|
||||
promises.push(client.close());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this._self.#idleClients.reset();
|
||||
this._self.#clientsInUse.reset();
|
||||
} catch (err) {
|
||||
|
||||
} finally {
|
||||
this._self.#isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const client of this._self.#idleClients) {
|
||||
client.destroy();
|
||||
}
|
||||
this._self.#idleClients.reset();
|
||||
|
||||
for (const client of this._self.#clientsInUse) {
|
||||
client.destroy();
|
||||
}
|
||||
this._self.#clientsInUse.reset();
|
||||
|
||||
this._self.#isOpen = false;
|
||||
}
|
||||
}
|
@@ -1,151 +1,151 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { PubSub, PubSubType } from './pub-sub';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { PubSub, PUBSUB_TYPE } from './pub-sub';
|
||||
|
||||
describe('PubSub', () => {
|
||||
const TYPE = PubSubType.CHANNELS,
|
||||
CHANNEL = 'channel',
|
||||
LISTENER = () => {};
|
||||
const TYPE = PUBSUB_TYPE.CHANNELS,
|
||||
CHANNEL = 'channel',
|
||||
LISTENER = () => {};
|
||||
|
||||
describe('subscribe to new channel', () => {
|
||||
function createAndSubscribe() {
|
||||
const pubSub = new PubSub(),
|
||||
command = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(command);
|
||||
assert.equal(command.channelsCounter, 1);
|
||||
|
||||
return {
|
||||
pubSub,
|
||||
command
|
||||
};
|
||||
}
|
||||
describe('subscribe to new channel', () => {
|
||||
function createAndSubscribe() {
|
||||
const pubSub = new PubSub(),
|
||||
command = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
|
||||
it('resolve', () => {
|
||||
const { pubSub, command } = createAndSubscribe();
|
||||
|
||||
command.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(command);
|
||||
assert.equal(command.channelsCounter, 1);
|
||||
|
||||
assert.equal(pubSub.isActive, true);
|
||||
});
|
||||
return {
|
||||
pubSub,
|
||||
command
|
||||
};
|
||||
}
|
||||
|
||||
it('reject', () => {
|
||||
const { pubSub, command } = createAndSubscribe();
|
||||
|
||||
assert.ok(command.reject);
|
||||
command.reject();
|
||||
it('resolve', () => {
|
||||
const { pubSub, command } = createAndSubscribe();
|
||||
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
command.resolve();
|
||||
|
||||
assert.equal(pubSub.isActive, true);
|
||||
});
|
||||
|
||||
it('subscribe to already subscribed channel', () => {
|
||||
it('reject', () => {
|
||||
const { pubSub, command } = createAndSubscribe();
|
||||
|
||||
assert.ok(command.reject);
|
||||
command.reject();
|
||||
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribe to already subscribed channel', () => {
|
||||
const pubSub = new PubSub(),
|
||||
firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(firstSubscribe);
|
||||
|
||||
const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(secondSubscribe);
|
||||
|
||||
firstSubscribe.resolve();
|
||||
|
||||
assert.equal(
|
||||
pubSub.subscribe(TYPE, CHANNEL, LISTENER),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('unsubscribe all', () => {
|
||||
const pubSub = new PubSub();
|
||||
|
||||
const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE);
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
|
||||
describe('unsubscribe from channel', () => {
|
||||
it('when not subscribed', () => {
|
||||
const pubSub = new PubSub(),
|
||||
unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
|
||||
it('when already subscribed', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribe from listener', () => {
|
||||
it('when it\'s the only listener', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
|
||||
it('when there are more listeners', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
assert.equal(
|
||||
pubSub.subscribe(TYPE, CHANNEL, () => { }),
|
||||
undefined
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
pubSub.unsubscribe(TYPE, CHANNEL, LISTENER),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
describe('non-existing listener', () => {
|
||||
it('on subscribed channel', () => {
|
||||
const pubSub = new PubSub(),
|
||||
firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(firstSubscribe);
|
||||
|
||||
const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(secondSubscribe);
|
||||
|
||||
firstSubscribe.resolve();
|
||||
|
||||
assert.equal(
|
||||
pubSub.subscribe(TYPE, CHANNEL, LISTENER),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('unsubscribe all', () => {
|
||||
const pubSub = new PubSub();
|
||||
|
||||
const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE);
|
||||
assert.equal(
|
||||
pubSub.unsubscribe(TYPE, CHANNEL, () => { }),
|
||||
undefined
|
||||
);
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
});
|
||||
|
||||
it('on unsubscribed channel', () => {
|
||||
const pubSub = new PubSub();
|
||||
assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => { }));
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribe from channel', () => {
|
||||
it('when not subscribed', () => {
|
||||
const pubSub = new PubSub(),
|
||||
unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
|
||||
it('when already subscribed', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||
assert.equal(pubSub.isActive, true);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribe from listener', () => {
|
||||
it('when it\'s the only listener', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(unsubscribe);
|
||||
unsubscribe.resolve();
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
|
||||
it('when there are more listeners', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
assert.equal(
|
||||
pubSub.subscribe(TYPE, CHANNEL, () => {}),
|
||||
undefined
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
pubSub.unsubscribe(TYPE, CHANNEL, LISTENER),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
describe('non-existing listener', () => {
|
||||
it('on subscribed channel', () => {
|
||||
const pubSub = new PubSub(),
|
||||
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||
assert.ok(subscribe);
|
||||
subscribe.resolve();
|
||||
assert.equal(pubSub.isActive, true);
|
||||
|
||||
assert.equal(
|
||||
pubSub.unsubscribe(TYPE, CHANNEL, () => {}),
|
||||
undefined
|
||||
);
|
||||
assert.equal(pubSub.isActive, true);
|
||||
});
|
||||
|
||||
it('on unsubscribed channel', () => {
|
||||
const pubSub = new PubSub();
|
||||
assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => {}));
|
||||
assert.equal(pubSub.isActive, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,408 +1,409 @@
|
||||
import { RedisCommandArgument } from "../commands";
|
||||
import { RedisArgument } from '../RESP/types';
|
||||
import { CommandToWrite } from './commands-queue';
|
||||
|
||||
export enum PubSubType {
|
||||
CHANNELS = 'CHANNELS',
|
||||
PATTERNS = 'PATTERNS',
|
||||
SHARDED = 'SHARDED'
|
||||
}
|
||||
export const PUBSUB_TYPE = {
|
||||
CHANNELS: 'CHANNELS',
|
||||
PATTERNS: 'PATTERNS',
|
||||
SHARDED: 'SHARDED'
|
||||
} as const;
|
||||
|
||||
export type PUBSUB_TYPE = typeof PUBSUB_TYPE;
|
||||
|
||||
export type PubSubType = PUBSUB_TYPE[keyof PUBSUB_TYPE];
|
||||
|
||||
const COMMANDS = {
|
||||
[PubSubType.CHANNELS]: {
|
||||
subscribe: Buffer.from('subscribe'),
|
||||
unsubscribe: Buffer.from('unsubscribe'),
|
||||
message: Buffer.from('message')
|
||||
},
|
||||
[PubSubType.PATTERNS]: {
|
||||
subscribe: Buffer.from('psubscribe'),
|
||||
unsubscribe: Buffer.from('punsubscribe'),
|
||||
message: Buffer.from('pmessage')
|
||||
},
|
||||
[PubSubType.SHARDED]: {
|
||||
subscribe: Buffer.from('ssubscribe'),
|
||||
unsubscribe: Buffer.from('sunsubscribe'),
|
||||
message: Buffer.from('smessage')
|
||||
}
|
||||
[PUBSUB_TYPE.CHANNELS]: {
|
||||
subscribe: Buffer.from('subscribe'),
|
||||
unsubscribe: Buffer.from('unsubscribe'),
|
||||
message: Buffer.from('message')
|
||||
},
|
||||
[PUBSUB_TYPE.PATTERNS]: {
|
||||
subscribe: Buffer.from('psubscribe'),
|
||||
unsubscribe: Buffer.from('punsubscribe'),
|
||||
message: Buffer.from('pmessage')
|
||||
},
|
||||
[PUBSUB_TYPE.SHARDED]: {
|
||||
subscribe: Buffer.from('ssubscribe'),
|
||||
unsubscribe: Buffer.from('sunsubscribe'),
|
||||
message: Buffer.from('smessage')
|
||||
}
|
||||
};
|
||||
|
||||
export type PubSubListener<
|
||||
RETURN_BUFFERS extends boolean = false
|
||||
RETURN_BUFFERS extends boolean = false
|
||||
> = <T extends RETURN_BUFFERS extends true ? Buffer : string>(message: T, channel: T) => unknown;
|
||||
|
||||
export interface ChannelListeners {
|
||||
unsubscribing: boolean;
|
||||
buffers: Set<PubSubListener<true>>;
|
||||
strings: Set<PubSubListener<false>>;
|
||||
unsubscribing: boolean;
|
||||
buffers: Set<PubSubListener<true>>;
|
||||
strings: Set<PubSubListener<false>>;
|
||||
}
|
||||
|
||||
export type PubSubTypeListeners = Map<string, ChannelListeners>;
|
||||
|
||||
type Listeners = Record<PubSubType, PubSubTypeListeners>;
|
||||
export type PubSubListeners = Record<PubSubType, PubSubTypeListeners>;
|
||||
|
||||
export type PubSubCommand = ReturnType<
|
||||
typeof PubSub.prototype.subscribe |
|
||||
typeof PubSub.prototype.unsubscribe |
|
||||
typeof PubSub.prototype.extendTypeListeners
|
||||
>;
|
||||
export type PubSubCommand = (
|
||||
Required<Pick<CommandToWrite, 'args' | 'channelsCounter' | 'resolve'>> & {
|
||||
reject: undefined | (() => unknown);
|
||||
}
|
||||
);
|
||||
|
||||
export class PubSub {
|
||||
static isStatusReply(reply: Array<Buffer>): boolean {
|
||||
return (
|
||||
COMMANDS[PubSubType.CHANNELS].subscribe.equals(reply[0]) ||
|
||||
COMMANDS[PubSubType.CHANNELS].unsubscribe.equals(reply[0]) ||
|
||||
COMMANDS[PubSubType.PATTERNS].subscribe.equals(reply[0]) ||
|
||||
COMMANDS[PubSubType.PATTERNS].unsubscribe.equals(reply[0]) ||
|
||||
COMMANDS[PubSubType.SHARDED].subscribe.equals(reply[0])
|
||||
);
|
||||
static isStatusReply(reply: Array<Buffer>): boolean {
|
||||
return (
|
||||
COMMANDS[PUBSUB_TYPE.CHANNELS].subscribe.equals(reply[0]) ||
|
||||
COMMANDS[PUBSUB_TYPE.CHANNELS].unsubscribe.equals(reply[0]) ||
|
||||
COMMANDS[PUBSUB_TYPE.PATTERNS].subscribe.equals(reply[0]) ||
|
||||
COMMANDS[PUBSUB_TYPE.PATTERNS].unsubscribe.equals(reply[0]) ||
|
||||
COMMANDS[PUBSUB_TYPE.SHARDED].subscribe.equals(reply[0])
|
||||
);
|
||||
}
|
||||
|
||||
static isShardedUnsubscribe(reply: Array<Buffer>): boolean {
|
||||
return COMMANDS[PUBSUB_TYPE.SHARDED].unsubscribe.equals(reply[0]);
|
||||
}
|
||||
|
||||
static #channelsArray(channels: string | Array<string>) {
|
||||
return (Array.isArray(channels) ? channels : [channels]);
|
||||
}
|
||||
|
||||
static #listenersSet<T extends boolean>(
|
||||
listeners: ChannelListeners,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
return (returnBuffers ? listeners.buffers : listeners.strings);
|
||||
}
|
||||
|
||||
#subscribing = 0;
|
||||
|
||||
#isActive = false;
|
||||
|
||||
get isActive() {
|
||||
return this.#isActive;
|
||||
}
|
||||
|
||||
readonly listeners: PubSubListeners = {
|
||||
[PUBSUB_TYPE.CHANNELS]: new Map(),
|
||||
[PUBSUB_TYPE.PATTERNS]: new Map(),
|
||||
[PUBSUB_TYPE.SHARDED]: new Map()
|
||||
};
|
||||
|
||||
subscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels: string | Array<string>,
|
||||
listener: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const args: Array<RedisArgument> = [COMMANDS[type].subscribe],
|
||||
channelsArray = PubSub.#channelsArray(channels);
|
||||
for (const channel of channelsArray) {
|
||||
let channelListeners = this.listeners[type].get(channel);
|
||||
if (!channelListeners || channelListeners.unsubscribing) {
|
||||
args.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
static isShardedUnsubscribe(reply: Array<Buffer>): boolean {
|
||||
return COMMANDS[PubSubType.SHARDED].unsubscribe.equals(reply[0]);
|
||||
}
|
||||
|
||||
static #channelsArray(channels: string | Array<string>) {
|
||||
return (Array.isArray(channels) ? channels : [channels]);
|
||||
if (args.length === 1) {
|
||||
// all channels are already subscribed, add listeners without issuing a command
|
||||
for (const channel of channelsArray) {
|
||||
PubSub.#listenersSet(
|
||||
this.listeners[type].get(channel)!,
|
||||
returnBuffers
|
||||
).add(listener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static #listenersSet<T extends boolean>(
|
||||
listeners: ChannelListeners,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
return (returnBuffers ? listeners.buffers : listeners.strings);
|
||||
}
|
||||
|
||||
#subscribing = 0;
|
||||
|
||||
#isActive = false;
|
||||
|
||||
get isActive() {
|
||||
return this.#isActive;
|
||||
}
|
||||
|
||||
#listeners: Listeners = {
|
||||
[PubSubType.CHANNELS]: new Map(),
|
||||
[PubSubType.PATTERNS]: new Map(),
|
||||
[PubSubType.SHARDED]: new Map()
|
||||
};
|
||||
|
||||
subscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels: string | Array<string>,
|
||||
listener: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe],
|
||||
channelsArray = PubSub.#channelsArray(channels);
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args,
|
||||
channelsCounter: args.length - 1,
|
||||
resolve: () => {
|
||||
this.#subscribing--;
|
||||
for (const channel of channelsArray) {
|
||||
let channelListeners = this.#listeners[type].get(channel);
|
||||
if (!channelListeners || channelListeners.unsubscribing) {
|
||||
args.push(channel);
|
||||
}
|
||||
let listeners = this.listeners[type].get(channel);
|
||||
if (!listeners) {
|
||||
listeners = {
|
||||
unsubscribing: false,
|
||||
buffers: new Set(),
|
||||
strings: new Set()
|
||||
};
|
||||
this.listeners[type].set(channel, listeners);
|
||||
}
|
||||
|
||||
PubSub.#listenersSet(listeners, returnBuffers).add(listener);
|
||||
}
|
||||
|
||||
if (args.length === 1) {
|
||||
// all channels are already subscribed, add listeners without issuing a command
|
||||
for (const channel of channelsArray) {
|
||||
PubSub.#listenersSet(
|
||||
this.#listeners[type].get(channel)!,
|
||||
returnBuffers
|
||||
).add(listener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args,
|
||||
channelsCounter: args.length - 1,
|
||||
resolve: () => {
|
||||
this.#subscribing--;
|
||||
for (const channel of channelsArray) {
|
||||
let listeners = this.#listeners[type].get(channel);
|
||||
if (!listeners) {
|
||||
listeners = {
|
||||
unsubscribing: false,
|
||||
buffers: new Set(),
|
||||
strings: new Set()
|
||||
};
|
||||
this.#listeners[type].set(channel, listeners);
|
||||
}
|
||||
|
||||
PubSub.#listenersSet(listeners, returnBuffers).add(listener);
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extendChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
if (!this.#extendChannelListeners(type, channel, listeners)) return;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args: [
|
||||
COMMANDS[type].subscribe,
|
||||
channel
|
||||
],
|
||||
channelsCounter: 1,
|
||||
resolve: () => this.#subscribing--,
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#extendChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
const existingListeners = this.#listeners[type].get(channel);
|
||||
if (!existingListeners) {
|
||||
this.#listeners[type].set(channel, listeners);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const listener of listeners.buffers) {
|
||||
existingListeners.buffers.add(listener);
|
||||
}
|
||||
|
||||
for (const listener of listeners.strings) {
|
||||
existingListeners.strings.add(listener);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe];
|
||||
for (const [channel, channelListeners] of listeners) {
|
||||
if (this.#extendChannelListeners(type, channel, channelListeners)) {
|
||||
args.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length === 1) return;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args,
|
||||
channelsCounter: args.length - 1,
|
||||
resolve: () => this.#subscribing--,
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
unsubscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels?: string | Array<string>,
|
||||
listener?: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const listeners = this.#listeners[type];
|
||||
if (!channels) {
|
||||
return this.#unsubscribeCommand(
|
||||
[COMMANDS[type].unsubscribe],
|
||||
// cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue
|
||||
// cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail
|
||||
NaN,
|
||||
() => listeners.clear()
|
||||
);
|
||||
}
|
||||
|
||||
const channelsArray = PubSub.#channelsArray(channels);
|
||||
if (!listener) {
|
||||
return this.#unsubscribeCommand(
|
||||
[COMMANDS[type].unsubscribe, ...channelsArray],
|
||||
channelsArray.length,
|
||||
() => {
|
||||
for (const channel of channelsArray) {
|
||||
listeners.delete(channel);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const args: Array<RedisCommandArgument> = [COMMANDS[type].unsubscribe];
|
||||
for (const channel of channelsArray) {
|
||||
const sets = listeners.get(channel);
|
||||
if (sets) {
|
||||
let current,
|
||||
other;
|
||||
if (returnBuffers) {
|
||||
current = sets.buffers;
|
||||
other = sets.strings;
|
||||
} else {
|
||||
current = sets.strings;
|
||||
other = sets.buffers;
|
||||
}
|
||||
|
||||
const currentSize = current.has(listener) ? current.size - 1 : current.size;
|
||||
if (currentSize !== 0 || other.size !== 0) continue;
|
||||
sets.unsubscribing = true;
|
||||
}
|
||||
|
||||
args.push(channel);
|
||||
}
|
||||
|
||||
if (args.length === 1) {
|
||||
// all channels has other listeners,
|
||||
// delete the listeners without issuing a command
|
||||
for (const channel of channelsArray) {
|
||||
PubSub.#listenersSet(
|
||||
listeners.get(channel)!,
|
||||
returnBuffers
|
||||
).delete(listener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#unsubscribeCommand(
|
||||
args,
|
||||
args.length - 1,
|
||||
() => {
|
||||
for (const channel of channelsArray) {
|
||||
const sets = listeners.get(channel);
|
||||
if (!sets) continue;
|
||||
|
||||
(returnBuffers ? sets.buffers : sets.strings).delete(listener);
|
||||
if (sets.buffers.size === 0 && sets.strings.size === 0) {
|
||||
listeners.delete(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#unsubscribeCommand(
|
||||
args: Array<RedisCommandArgument>,
|
||||
channelsCounter: number,
|
||||
removeListeners: () => void
|
||||
) {
|
||||
return {
|
||||
args,
|
||||
channelsCounter,
|
||||
resolve: () => {
|
||||
removeListeners();
|
||||
this.#updateIsActive();
|
||||
},
|
||||
reject: undefined // use the same structure as `subscribe`
|
||||
};
|
||||
}
|
||||
|
||||
#updateIsActive() {
|
||||
this.#isActive = (
|
||||
this.#listeners[PubSubType.CHANNELS].size !== 0 ||
|
||||
this.#listeners[PubSubType.PATTERNS].size !== 0 ||
|
||||
this.#listeners[PubSubType.SHARDED].size !== 0 ||
|
||||
this.#subscribing !== 0
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#isActive = false;
|
||||
this.#subscribing = 0;
|
||||
}
|
||||
|
||||
resubscribe(): Array<PubSubCommand> {
|
||||
const commands = [];
|
||||
for (const [type, listeners] of Object.entries(this.#listeners)) {
|
||||
if (!listeners.size) continue;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
const callback = () => this.#subscribing--;
|
||||
commands.push({
|
||||
args: [
|
||||
COMMANDS[type as PubSubType].subscribe,
|
||||
...listeners.keys()
|
||||
],
|
||||
channelsCounter: listeners.size,
|
||||
resolve: callback,
|
||||
reject: callback
|
||||
});
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
handleMessageReply(reply: Array<Buffer>): boolean {
|
||||
if (COMMANDS[PubSubType.CHANNELS].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PubSubType.CHANNELS,
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
} else if (COMMANDS[PubSubType.PATTERNS].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PubSubType.PATTERNS,
|
||||
reply[3],
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
} else if (COMMANDS[PubSubType.SHARDED].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PubSubType.SHARDED,
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
removeShardedListeners(channel: string): ChannelListeners {
|
||||
const listeners = this.#listeners[PubSubType.SHARDED].get(channel)!;
|
||||
this.#listeners[PubSubType.SHARDED].delete(channel);
|
||||
},
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
return listeners;
|
||||
}
|
||||
} satisfies PubSubCommand;
|
||||
}
|
||||
|
||||
extendChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
if (!this.#extendChannelListeners(type, channel, listeners)) return;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args: [
|
||||
COMMANDS[type].subscribe,
|
||||
channel
|
||||
],
|
||||
channelsCounter: 1,
|
||||
resolve: () => this.#subscribing--,
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
}
|
||||
} satisfies PubSubCommand;
|
||||
}
|
||||
|
||||
#extendChannelListeners(
|
||||
type: PubSubType,
|
||||
channel: string,
|
||||
listeners: ChannelListeners
|
||||
) {
|
||||
const existingListeners = this.listeners[type].get(channel);
|
||||
if (!existingListeners) {
|
||||
this.listeners[type].set(channel, listeners);
|
||||
return true;
|
||||
}
|
||||
|
||||
#emitPubSubMessage(
|
||||
type: PubSubType,
|
||||
message: Buffer,
|
||||
channel: Buffer,
|
||||
pattern?: Buffer
|
||||
): void {
|
||||
const keyString = (pattern ?? channel).toString(),
|
||||
listeners = this.#listeners[type].get(keyString);
|
||||
|
||||
if (!listeners) return;
|
||||
for (const listener of listeners.buffers) {
|
||||
existingListeners.buffers.add(listener);
|
||||
}
|
||||
|
||||
for (const listener of listeners.buffers) {
|
||||
listener(message, channel);
|
||||
for (const listener of listeners.strings) {
|
||||
existingListeners.strings.add(listener);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||
const args: Array<RedisArgument> = [COMMANDS[type].subscribe];
|
||||
for (const [channel, channelListeners] of listeners) {
|
||||
if (this.#extendChannelListeners(type, channel, channelListeners)) {
|
||||
args.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length === 1) return;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
return {
|
||||
args,
|
||||
channelsCounter: args.length - 1,
|
||||
resolve: () => this.#subscribing--,
|
||||
reject: () => {
|
||||
this.#subscribing--;
|
||||
this.#updateIsActive();
|
||||
}
|
||||
} satisfies PubSubCommand;
|
||||
}
|
||||
|
||||
unsubscribe<T extends boolean>(
|
||||
type: PubSubType,
|
||||
channels?: string | Array<string>,
|
||||
listener?: PubSubListener<T>,
|
||||
returnBuffers?: T
|
||||
) {
|
||||
const listeners = this.listeners[type];
|
||||
if (!channels) {
|
||||
return this.#unsubscribeCommand(
|
||||
[COMMANDS[type].unsubscribe],
|
||||
// cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue
|
||||
// cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail
|
||||
NaN,
|
||||
() => listeners.clear()
|
||||
);
|
||||
}
|
||||
|
||||
const channelsArray = PubSub.#channelsArray(channels);
|
||||
if (!listener) {
|
||||
return this.#unsubscribeCommand(
|
||||
[COMMANDS[type].unsubscribe, ...channelsArray],
|
||||
channelsArray.length,
|
||||
() => {
|
||||
for (const channel of channelsArray) {
|
||||
listeners.delete(channel);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const args: Array<RedisArgument> = [COMMANDS[type].unsubscribe];
|
||||
for (const channel of channelsArray) {
|
||||
const sets = listeners.get(channel);
|
||||
if (sets) {
|
||||
let current,
|
||||
other;
|
||||
if (returnBuffers) {
|
||||
current = sets.buffers;
|
||||
other = sets.strings;
|
||||
} else {
|
||||
current = sets.strings;
|
||||
other = sets.buffers;
|
||||
}
|
||||
|
||||
if (!listeners.strings.size) return;
|
||||
const currentSize = current.has(listener) ? current.size - 1 : current.size;
|
||||
if (currentSize !== 0 || other.size !== 0) continue;
|
||||
sets.unsubscribing = true;
|
||||
}
|
||||
|
||||
const channelString = pattern ? channel.toString() : keyString,
|
||||
messageString = channelString === '__redis__:invalidate' ?
|
||||
// https://github.com/redis/redis/pull/7469
|
||||
// https://github.com/redis/redis/issues/7463
|
||||
(message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
|
||||
message.toString();
|
||||
for (const listener of listeners.strings) {
|
||||
listener(messageString, channelString);
|
||||
args.push(channel);
|
||||
}
|
||||
|
||||
if (args.length === 1) {
|
||||
// all channels has other listeners,
|
||||
// delete the listeners without issuing a command
|
||||
for (const channel of channelsArray) {
|
||||
PubSub.#listenersSet(
|
||||
listeners.get(channel)!,
|
||||
returnBuffers
|
||||
).delete(listener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#unsubscribeCommand(
|
||||
args,
|
||||
args.length - 1,
|
||||
() => {
|
||||
for (const channel of channelsArray) {
|
||||
const sets = listeners.get(channel);
|
||||
if (!sets) continue;
|
||||
|
||||
(returnBuffers ? sets.buffers : sets.strings).delete(listener);
|
||||
if (sets.buffers.size === 0 && sets.strings.size === 0) {
|
||||
listeners.delete(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#unsubscribeCommand(
|
||||
args: Array<RedisArgument>,
|
||||
channelsCounter: number,
|
||||
removeListeners: () => void
|
||||
) {
|
||||
return {
|
||||
args,
|
||||
channelsCounter,
|
||||
resolve: () => {
|
||||
removeListeners();
|
||||
this.#updateIsActive();
|
||||
},
|
||||
reject: undefined
|
||||
} satisfies PubSubCommand;
|
||||
}
|
||||
|
||||
#updateIsActive() {
|
||||
this.#isActive = (
|
||||
this.listeners[PUBSUB_TYPE.CHANNELS].size !== 0 ||
|
||||
this.listeners[PUBSUB_TYPE.PATTERNS].size !== 0 ||
|
||||
this.listeners[PUBSUB_TYPE.SHARDED].size !== 0 ||
|
||||
this.#subscribing !== 0
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#isActive = false;
|
||||
this.#subscribing = 0;
|
||||
}
|
||||
|
||||
resubscribe() {
|
||||
const commands = [];
|
||||
for (const [type, listeners] of Object.entries(this.listeners)) {
|
||||
if (!listeners.size) continue;
|
||||
|
||||
this.#isActive = true;
|
||||
this.#subscribing++;
|
||||
const callback = () => this.#subscribing--;
|
||||
commands.push({
|
||||
args: [
|
||||
COMMANDS[type as PubSubType].subscribe,
|
||||
...listeners.keys()
|
||||
],
|
||||
channelsCounter: listeners.size,
|
||||
resolve: callback,
|
||||
reject: callback
|
||||
} satisfies PubSubCommand);
|
||||
}
|
||||
|
||||
getTypeListeners(type: PubSubType): PubSubTypeListeners {
|
||||
return this.#listeners[type];
|
||||
return commands;
|
||||
}
|
||||
|
||||
handleMessageReply(reply: Array<Buffer>): boolean {
|
||||
if (COMMANDS[PUBSUB_TYPE.CHANNELS].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PUBSUB_TYPE.CHANNELS,
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
} else if (COMMANDS[PUBSUB_TYPE.PATTERNS].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PUBSUB_TYPE.PATTERNS,
|
||||
reply[3],
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
} else if (COMMANDS[PUBSUB_TYPE.SHARDED].message.equals(reply[0])) {
|
||||
this.#emitPubSubMessage(
|
||||
PUBSUB_TYPE.SHARDED,
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
removeShardedListeners(channel: string): ChannelListeners {
|
||||
const listeners = this.listeners[PUBSUB_TYPE.SHARDED].get(channel)!;
|
||||
this.listeners[PUBSUB_TYPE.SHARDED].delete(channel);
|
||||
this.#updateIsActive();
|
||||
return listeners;
|
||||
}
|
||||
|
||||
#emitPubSubMessage(
|
||||
type: PubSubType,
|
||||
message: Buffer,
|
||||
channel: Buffer,
|
||||
pattern?: Buffer
|
||||
): void {
|
||||
const keyString = (pattern ?? channel).toString(),
|
||||
listeners = this.listeners[type].get(keyString);
|
||||
|
||||
if (!listeners) return;
|
||||
|
||||
for (const listener of listeners.buffers) {
|
||||
listener(message, channel);
|
||||
}
|
||||
|
||||
if (!listeners.strings.size) return;
|
||||
|
||||
const channelString = pattern ? channel.toString() : keyString,
|
||||
messageString = channelString === '__redis__:invalidate' ?
|
||||
// https://github.com/redis/redis/pull/7469
|
||||
// https://github.com/redis/redis/issues/7463
|
||||
(message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
|
||||
message.toString();
|
||||
for (const listener of listeners.strings) {
|
||||
listener(messageString, channelString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,87 +1,87 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spy } from 'sinon';
|
||||
import { once } from 'events';
|
||||
import { once } from 'node:events';
|
||||
import RedisSocket, { RedisSocketOptions } from './socket';
|
||||
|
||||
describe('Socket', () => {
|
||||
function createSocket(options: RedisSocketOptions): RedisSocket {
|
||||
const socket = new RedisSocket(
|
||||
() => Promise.resolve(),
|
||||
options
|
||||
);
|
||||
function createSocket(options: RedisSocketOptions): RedisSocket {
|
||||
const socket = new RedisSocket(
|
||||
() => Promise.resolve(),
|
||||
options
|
||||
);
|
||||
|
||||
socket.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
describe('reconnectStrategy', () => {
|
||||
it('false', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy: false
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect());
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('0', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy: 0
|
||||
});
|
||||
|
||||
socket.connect();
|
||||
await once(socket, 'error');
|
||||
assert.equal(socket.isOpen, true);
|
||||
assert.equal(socket.isReady, false);
|
||||
socket.disconnect();
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('custom strategy', async () => {
|
||||
const numberOfRetries = 3;
|
||||
|
||||
const reconnectStrategy = spy((retries: number) => {
|
||||
assert.equal(retries + 1, reconnectStrategy.callCount);
|
||||
|
||||
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect(), {
|
||||
message: `${numberOfRetries}`
|
||||
});
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy(retries: number) {
|
||||
if (retries === 1) return new Error('done');
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect());
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
socket.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
describe('reconnectStrategy', () => {
|
||||
it('false', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy: false
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect());
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('0', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy: 0
|
||||
});
|
||||
|
||||
socket.connect();
|
||||
await once(socket, 'error');
|
||||
assert.equal(socket.isOpen, true);
|
||||
assert.equal(socket.isReady, false);
|
||||
socket.destroy();
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('custom strategy', async () => {
|
||||
const numberOfRetries = 3;
|
||||
|
||||
const reconnectStrategy = spy((retries: number) => {
|
||||
assert.equal(retries + 1, reconnectStrategy.callCount);
|
||||
|
||||
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect(), {
|
||||
message: `${numberOfRetries}`
|
||||
});
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const socket = createSocket({
|
||||
host: 'error',
|
||||
connectTimeout: 1,
|
||||
reconnectStrategy(retries: number) {
|
||||
if (retries === 1) return new Error('done');
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(socket.connect());
|
||||
|
||||
assert.equal(socket.isOpen, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,310 +1,345 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
import { RedisCommandArguments } from '../commands';
|
||||
import { EventEmitter, once } from 'node:events';
|
||||
import net from 'node:net';
|
||||
import tls from 'node:tls';
|
||||
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors';
|
||||
import { promiseTimeout } from '../utils';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { RedisArgument } from '../RESP/types';
|
||||
|
||||
export interface RedisSocketCommonOptions {
|
||||
/**
|
||||
* Connection Timeout (in milliseconds)
|
||||
*/
|
||||
connectTimeout?: number;
|
||||
/**
|
||||
* Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)
|
||||
*/
|
||||
noDelay?: boolean;
|
||||
/**
|
||||
* Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay)
|
||||
*/
|
||||
keepAlive?: number | false;
|
||||
/**
|
||||
* When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
|
||||
* 1. `false` -> do not reconnect, close the client and flush the command queue.
|
||||
* 2. `number` -> wait for `X` milliseconds before reconnecting.
|
||||
* 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
||||
* Defaults to `retries => Math.min(retries * 50, 500)`
|
||||
*/
|
||||
reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number);
|
||||
}
|
||||
|
||||
type RedisNetSocketOptions = Partial<net.SocketConnectOpts> & {
|
||||
tls?: false;
|
||||
type NetOptions = {
|
||||
tls?: false;
|
||||
};
|
||||
|
||||
export interface RedisTlsSocketOptions extends tls.ConnectionOptions {
|
||||
tls: true;
|
||||
type ReconnectStrategyFunction = (retries: number, cause: Error) => false | Error | number;
|
||||
|
||||
type RedisSocketOptionsCommon = {
|
||||
/**
|
||||
* Connection timeout (in milliseconds)
|
||||
*/
|
||||
connectTimeout?: number;
|
||||
/**
|
||||
* When the socket closes unexpectedly (without calling `.close()`/`.destroy()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
|
||||
* 1. `false` -> do not reconnect, close the client and flush the command queue.
|
||||
* 2. `number` -> wait for `X` milliseconds before reconnecting.
|
||||
* 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
||||
*/
|
||||
reconnectStrategy?: false | number | ReconnectStrategyFunction;
|
||||
}
|
||||
|
||||
export type RedisSocketOptions = RedisSocketCommonOptions & (RedisNetSocketOptions | RedisTlsSocketOptions);
|
||||
type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit<
|
||||
net.TcpNetConnectOpts,
|
||||
'timeout' | 'onread' | 'readable' | 'writable' | 'port'
|
||||
> & {
|
||||
port?: number;
|
||||
};
|
||||
|
||||
interface CreateSocketReturn<T> {
|
||||
connectEvent: string;
|
||||
socket: T;
|
||||
type RedisTlsOptions = RedisSocketOptionsCommon & tls.ConnectionOptions & {
|
||||
tls: true;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export type RedisSocketInitiator = () => Promise<void>;
|
||||
type RedisIpcOptions = RedisSocketOptionsCommon & Omit<
|
||||
net.IpcNetConnectOpts,
|
||||
'timeout' | 'onread' | 'readable' | 'writable'
|
||||
> & {
|
||||
tls: false;
|
||||
}
|
||||
|
||||
export type RedisTcpSocketOptions = RedisTcpOptions | RedisTlsOptions;
|
||||
|
||||
export type RedisSocketOptions = RedisTcpSocketOptions | RedisIpcOptions;
|
||||
|
||||
export type RedisSocketInitiator = () => void | Promise<unknown>;
|
||||
|
||||
export default class RedisSocket extends EventEmitter {
|
||||
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
|
||||
options ??= {};
|
||||
if (!(options as net.IpcSocketConnectOpts).path) {
|
||||
(options as net.TcpSocketConnectOpts).port ??= 6379;
|
||||
(options as net.TcpSocketConnectOpts).host ??= 'localhost';
|
||||
readonly #initiator;
|
||||
readonly #connectTimeout;
|
||||
readonly #reconnectStrategy;
|
||||
readonly #socketFactory;
|
||||
|
||||
#socket?: net.Socket | tls.TLSSocket;
|
||||
|
||||
#isOpen = false;
|
||||
|
||||
get isOpen() {
|
||||
return this.#isOpen;
|
||||
}
|
||||
|
||||
#isReady = false;
|
||||
|
||||
get isReady() {
|
||||
return this.#isReady;
|
||||
}
|
||||
|
||||
#isSocketUnrefed = false;
|
||||
|
||||
constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) {
|
||||
super();
|
||||
|
||||
this.#initiator = initiator;
|
||||
this.#connectTimeout = options?.connectTimeout ?? 5000;
|
||||
this.#reconnectStrategy = this.#createReconnectStrategy(options);
|
||||
this.#socketFactory = this.#createSocketFactory(options);
|
||||
}
|
||||
|
||||
#createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction {
|
||||
const strategy = options?.reconnectStrategy;
|
||||
if (strategy === false || typeof strategy === 'number') {
|
||||
return () => strategy;
|
||||
}
|
||||
|
||||
if (strategy) {
|
||||
return (retries, cause) => {
|
||||
try {
|
||||
const retryIn = strategy(retries, cause);
|
||||
if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
|
||||
throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
|
||||
}
|
||||
return retryIn;
|
||||
} catch (err) {
|
||||
this.emit('error', err);
|
||||
return this.defaultReconnectStrategy(retries);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return this.defaultReconnectStrategy;
|
||||
}
|
||||
|
||||
#createSocketFactory(options?: RedisSocketOptions) {
|
||||
// TLS
|
||||
if (options?.tls === true) {
|
||||
const withDefaults: tls.ConnectionOptions = {
|
||||
...options,
|
||||
port: options?.port ?? 6379,
|
||||
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
|
||||
// @types/node is... incorrect...
|
||||
// @ts-expect-error
|
||||
noDelay: options?.noDelay ?? true,
|
||||
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
|
||||
// @types/node is... incorrect...
|
||||
// @ts-expect-error
|
||||
keepAlive: options?.keepAlive ?? true,
|
||||
// https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed"
|
||||
// @types/node is... incorrect...
|
||||
// @ts-expect-error
|
||||
keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
|
||||
timeout: undefined,
|
||||
onread: undefined,
|
||||
readable: true,
|
||||
writable: true
|
||||
};
|
||||
return {
|
||||
create() {
|
||||
return tls.connect(withDefaults);
|
||||
},
|
||||
event: 'secureConnect'
|
||||
};
|
||||
}
|
||||
|
||||
// IPC
|
||||
if (options && 'path' in options) {
|
||||
const withDefaults: net.IpcNetConnectOpts = {
|
||||
...options,
|
||||
timeout: undefined,
|
||||
onread: undefined,
|
||||
readable: true,
|
||||
writable: true
|
||||
};
|
||||
return {
|
||||
create() {
|
||||
return net.createConnection(withDefaults);
|
||||
},
|
||||
event: 'connect'
|
||||
};
|
||||
}
|
||||
|
||||
// TCP
|
||||
const withDefaults: net.TcpNetConnectOpts = {
|
||||
...options,
|
||||
port: options?.port ?? 6379,
|
||||
noDelay: options?.noDelay ?? true,
|
||||
keepAlive: options?.keepAlive ?? true,
|
||||
keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000,
|
||||
timeout: undefined,
|
||||
onread: undefined,
|
||||
readable: true,
|
||||
writable: true
|
||||
};
|
||||
return {
|
||||
create() {
|
||||
return net.createConnection(withDefaults);
|
||||
},
|
||||
event: 'connect'
|
||||
};
|
||||
}
|
||||
|
||||
#shouldReconnect(retries: number, cause: Error) {
|
||||
const retryIn = this.#reconnectStrategy(retries, cause);
|
||||
if (retryIn === false) {
|
||||
this.#isOpen = false;
|
||||
this.emit('error', cause);
|
||||
return cause;
|
||||
} else if (retryIn instanceof Error) {
|
||||
this.#isOpen = false;
|
||||
this.emit('error', cause);
|
||||
return new ReconnectStrategyError(retryIn, cause);
|
||||
}
|
||||
|
||||
return retryIn;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.#isOpen) {
|
||||
throw new Error('Socket already opened');
|
||||
}
|
||||
|
||||
this.#isOpen = true;
|
||||
return this.#connect();
|
||||
}
|
||||
|
||||
async #connect(): Promise<void> {
|
||||
let retries = 0;
|
||||
do {
|
||||
try {
|
||||
this.#socket = await this.#createSocket();
|
||||
this.emit('connect');
|
||||
|
||||
try {
|
||||
await this.#initiator();
|
||||
} catch (err) {
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
throw err;
|
||||
}
|
||||
this.#isReady = true;
|
||||
this.emit('ready');
|
||||
} catch (err) {
|
||||
const retryIn = this.#shouldReconnect(retries++, err as Error);
|
||||
if (typeof retryIn !== 'number') {
|
||||
throw retryIn;
|
||||
}
|
||||
|
||||
options.connectTimeout ??= 5000;
|
||||
options.keepAlive ??= 5000;
|
||||
options.noDelay ??= true;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions {
|
||||
return (options as RedisTlsSocketOptions).tls === true;
|
||||
}
|
||||
|
||||
readonly #initiator: RedisSocketInitiator;
|
||||
|
||||
readonly #options: RedisSocketOptions;
|
||||
|
||||
#socket?: net.Socket | tls.TLSSocket;
|
||||
|
||||
#isOpen = false;
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.#isOpen;
|
||||
}
|
||||
|
||||
#isReady = false;
|
||||
|
||||
get isReady(): boolean {
|
||||
return this.#isReady;
|
||||
}
|
||||
|
||||
// `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
|
||||
// https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
|
||||
#writableNeedDrain = false;
|
||||
|
||||
get writableNeedDrain(): boolean {
|
||||
return this.#writableNeedDrain;
|
||||
}
|
||||
|
||||
#isSocketUnrefed = false;
|
||||
|
||||
constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) {
|
||||
super();
|
||||
|
||||
this.#initiator = initiator;
|
||||
this.#options = RedisSocket.#initiateOptions(options);
|
||||
}
|
||||
|
||||
#reconnectStrategy(retries: number, cause: Error) {
|
||||
if (this.#options.reconnectStrategy === false) {
|
||||
return false;
|
||||
} else if (typeof this.#options.reconnectStrategy === 'number') {
|
||||
return this.#options.reconnectStrategy;
|
||||
} else if (this.#options.reconnectStrategy) {
|
||||
try {
|
||||
const retryIn = this.#options.reconnectStrategy(retries, cause);
|
||||
if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
|
||||
throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
|
||||
}
|
||||
|
||||
return retryIn;
|
||||
} catch (err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(retries * 50, 500);
|
||||
}
|
||||
|
||||
#shouldReconnect(retries: number, cause: Error) {
|
||||
const retryIn = this.#reconnectStrategy(retries, cause);
|
||||
if (retryIn === false) {
|
||||
this.#isOpen = false;
|
||||
this.emit('error', cause);
|
||||
return cause;
|
||||
} else if (retryIn instanceof Error) {
|
||||
this.#isOpen = false;
|
||||
this.emit('error', cause);
|
||||
return new ReconnectStrategyError(retryIn, cause);
|
||||
}
|
||||
|
||||
return retryIn;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.#isOpen) {
|
||||
throw new Error('Socket already opened');
|
||||
}
|
||||
|
||||
this.#isOpen = true;
|
||||
return this.#connect();
|
||||
}
|
||||
|
||||
async #connect(): Promise<void> {
|
||||
let retries = 0;
|
||||
do {
|
||||
try {
|
||||
this.#socket = await this.#createSocket();
|
||||
this.#writableNeedDrain = false;
|
||||
this.emit('connect');
|
||||
|
||||
try {
|
||||
await this.#initiator();
|
||||
} catch (err) {
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
throw err;
|
||||
}
|
||||
this.#isReady = true;
|
||||
this.emit('ready');
|
||||
} catch (err) {
|
||||
const retryIn = this.#shouldReconnect(retries++, err as Error);
|
||||
if (typeof retryIn !== 'number') {
|
||||
throw retryIn;
|
||||
}
|
||||
|
||||
this.emit('error', err);
|
||||
await promiseTimeout(retryIn);
|
||||
this.emit('reconnecting');
|
||||
}
|
||||
} while (this.#isOpen && !this.#isReady);
|
||||
}
|
||||
|
||||
#createSocket(): Promise<net.Socket | tls.TLSSocket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { connectEvent, socket } = RedisSocket.#isTlsSocket(this.#options) ?
|
||||
this.#createTlsSocket() :
|
||||
this.#createNetSocket();
|
||||
|
||||
if (this.#options.connectTimeout) {
|
||||
socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError()));
|
||||
}
|
||||
|
||||
if (this.#isSocketUnrefed) {
|
||||
socket.unref();
|
||||
}
|
||||
|
||||
socket
|
||||
.setNoDelay(this.#options.noDelay)
|
||||
.once('error', reject)
|
||||
.once(connectEvent, () => {
|
||||
socket
|
||||
.setTimeout(0)
|
||||
// https://github.com/nodejs/node/issues/31663
|
||||
.setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0)
|
||||
.off('error', reject)
|
||||
.once('error', (err: Error) => this.#onSocketError(err))
|
||||
.once('close', hadError => {
|
||||
if (!hadError && this.#isOpen && this.#socket === socket) {
|
||||
this.#onSocketError(new SocketClosedUnexpectedlyError());
|
||||
}
|
||||
})
|
||||
.on('drain', () => {
|
||||
this.#writableNeedDrain = false;
|
||||
this.emit('drain');
|
||||
})
|
||||
.on('data', data => this.emit('data', data));
|
||||
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#createNetSocket(): CreateSocketReturn<net.Socket> {
|
||||
return {
|
||||
connectEvent: 'connect',
|
||||
socket: net.connect(this.#options as net.NetConnectOpts) // TODO
|
||||
};
|
||||
}
|
||||
|
||||
#createTlsSocket(): CreateSocketReturn<tls.TLSSocket> {
|
||||
return {
|
||||
connectEvent: 'secureConnect',
|
||||
socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO
|
||||
};
|
||||
}
|
||||
|
||||
#onSocketError(err: Error): void {
|
||||
const wasReady = this.#isReady;
|
||||
this.#isReady = false;
|
||||
this.emit('error', err);
|
||||
|
||||
if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
|
||||
|
||||
await setTimeout(retryIn);
|
||||
this.emit('reconnecting');
|
||||
this.#connect().catch(() => {
|
||||
// the error was already emitted, silently ignore it
|
||||
});
|
||||
}
|
||||
} while (this.#isOpen && !this.#isReady);
|
||||
}
|
||||
|
||||
async #createSocket(): Promise<net.Socket | tls.TLSSocket> {
|
||||
const socket = this.#socketFactory.create();
|
||||
|
||||
let onTimeout;
|
||||
if (this.#connectTimeout !== undefined) {
|
||||
onTimeout = () => socket.destroy(new ConnectionTimeoutError());
|
||||
socket.once('timeout', onTimeout);
|
||||
socket.setTimeout(this.#connectTimeout);
|
||||
}
|
||||
|
||||
writeCommand(args: RedisCommandArguments): void {
|
||||
if (!this.#socket) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
for (const toWrite of args) {
|
||||
this.#writableNeedDrain = !this.#socket.write(toWrite);
|
||||
}
|
||||
if (this.#isSocketUnrefed) {
|
||||
socket.unref();
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
await once(socket, this.#socketFactory.event);
|
||||
|
||||
this.#isOpen = false;
|
||||
this.#disconnect();
|
||||
if (onTimeout) {
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
}
|
||||
|
||||
#disconnect(): void {
|
||||
this.#isReady = false;
|
||||
socket
|
||||
.once('error', err => this.#onSocketError(err))
|
||||
.once('close', hadError => {
|
||||
if (hadError || !this.#isOpen || this.#socket !== socket) return;
|
||||
this.#onSocketError(new SocketClosedUnexpectedlyError());
|
||||
})
|
||||
.on('drain', () => this.emit('drain'))
|
||||
.on('data', data => this.emit('data', data));
|
||||
|
||||
if (this.#socket) {
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
}
|
||||
|
||||
this.emit('end');
|
||||
return socket;
|
||||
}
|
||||
|
||||
#onSocketError(err: Error): void {
|
||||
const wasReady = this.#isReady;
|
||||
this.#isReady = false;
|
||||
this.emit('error', err);
|
||||
|
||||
if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
|
||||
|
||||
this.emit('reconnecting');
|
||||
this.#connect().catch(() => {
|
||||
// the error was already emitted, silently ignore it
|
||||
});
|
||||
}
|
||||
|
||||
write(iterable: Iterable<Array<RedisArgument>>) {
|
||||
if (!this.#socket) return;
|
||||
|
||||
this.#socket.cork();
|
||||
for (const args of iterable) {
|
||||
for (const toWrite of args) {
|
||||
this.#socket.write(toWrite);
|
||||
}
|
||||
|
||||
if (this.#socket.writableNeedDrain) break;
|
||||
}
|
||||
this.#socket.uncork();
|
||||
}
|
||||
|
||||
async quit<T>(fn: () => Promise<T>): Promise<T> {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
async quit<T>(fn: () => Promise<T>): Promise<T> {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
this.#isOpen = false;
|
||||
const reply = await fn();
|
||||
this.destroySocket();
|
||||
return reply;
|
||||
}
|
||||
|
||||
this.#isOpen = false;
|
||||
const reply = await fn();
|
||||
this.#disconnect();
|
||||
return reply;
|
||||
close() {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
#isCorked = false;
|
||||
this.#isOpen = false;
|
||||
}
|
||||
|
||||
cork(): void {
|
||||
if (!this.#socket || this.#isCorked) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#socket.cork();
|
||||
this.#isCorked = true;
|
||||
|
||||
setImmediate(() => {
|
||||
this.#socket?.uncork();
|
||||
this.#isCorked = false;
|
||||
});
|
||||
destroy() {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
ref(): void {
|
||||
this.#isSocketUnrefed = false;
|
||||
this.#socket?.ref();
|
||||
this.#isOpen = false;
|
||||
this.destroySocket();
|
||||
}
|
||||
|
||||
destroySocket() {
|
||||
this.#isReady = false;
|
||||
|
||||
if (this.#socket) {
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
}
|
||||
|
||||
unref(): void {
|
||||
this.#isSocketUnrefed = true;
|
||||
this.#socket?.unref();
|
||||
}
|
||||
this.emit('end');
|
||||
}
|
||||
|
||||
ref() {
|
||||
this.#isSocketUnrefed = false;
|
||||
this.#socket?.ref();
|
||||
}
|
||||
|
||||
unref() {
|
||||
this.#isSocketUnrefed = true;
|
||||
this.#socket?.unref();
|
||||
}
|
||||
|
||||
defaultReconnectStrategy(retries: number) {
|
||||
// Generate a random jitter between 0 – 200 ms:
|
||||
const jitter = Math.floor(Math.random() * 200);
|
||||
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
|
||||
const delay = Math.min(Math.pow(2, retries) * 50, 2000);
|
||||
|
||||
return delay + jitter;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,670 +0,0 @@
|
||||
|
||||
import * as APPEND from '../commands/APPEND';
|
||||
import * as BITCOUNT from '../commands/BITCOUNT';
|
||||
import * as BITFIELD_RO from '../commands/BITFIELD_RO';
|
||||
import * as BITFIELD from '../commands/BITFIELD';
|
||||
import * as BITOP from '../commands/BITOP';
|
||||
import * as BITPOS from '../commands/BITPOS';
|
||||
import * as BLMOVE from '../commands/BLMOVE';
|
||||
import * as BLMPOP from '../commands/BLMPOP';
|
||||
import * as BLPOP from '../commands/BLPOP';
|
||||
import * as BRPOP from '../commands/BRPOP';
|
||||
import * as BRPOPLPUSH from '../commands/BRPOPLPUSH';
|
||||
import * as BZMPOP from '../commands/BZMPOP';
|
||||
import * as BZPOPMAX from '../commands/BZPOPMAX';
|
||||
import * as BZPOPMIN from '../commands/BZPOPMIN';
|
||||
import * as COPY from '../commands/COPY';
|
||||
import * as DECR from '../commands/DECR';
|
||||
import * as DECRBY from '../commands/DECRBY';
|
||||
import * as DEL from '../commands/DEL';
|
||||
import * as DUMP from '../commands/DUMP';
|
||||
import * as EVAL_RO from '../commands/EVAL_RO';
|
||||
import * as EVAL from '../commands/EVAL';
|
||||
import * as EVALSHA_RO from '../commands/EVALSHA_RO';
|
||||
import * as EVALSHA from '../commands/EVALSHA';
|
||||
import * as EXISTS from '../commands/EXISTS';
|
||||
import * as EXPIRE from '../commands/EXPIRE';
|
||||
import * as EXPIREAT from '../commands/EXPIREAT';
|
||||
import * as EXPIRETIME from '../commands/EXPIRETIME';
|
||||
import * as FCALL_RO from '../commands/FCALL_RO';
|
||||
import * as FCALL from '../commands/FCALL';
|
||||
import * as GEOADD from '../commands/GEOADD';
|
||||
import * as GEODIST from '../commands/GEODIST';
|
||||
import * as GEOHASH from '../commands/GEOHASH';
|
||||
import * as GEOPOS from '../commands/GEOPOS';
|
||||
import * as GEORADIUS_RO_WITH from '../commands/GEORADIUS_RO_WITH';
|
||||
import * as GEORADIUS_RO from '../commands/GEORADIUS_RO';
|
||||
import * as GEORADIUS_WITH from '../commands/GEORADIUS_WITH';
|
||||
import * as GEORADIUS from '../commands/GEORADIUS';
|
||||
import * as GEORADIUSBYMEMBER_RO_WITH from '../commands/GEORADIUSBYMEMBER_RO_WITH';
|
||||
import * as GEORADIUSBYMEMBER_RO from '../commands/GEORADIUSBYMEMBER_RO';
|
||||
import * as GEORADIUSBYMEMBER_WITH from '../commands/GEORADIUSBYMEMBER_WITH';
|
||||
import * as GEORADIUSBYMEMBER from '../commands/GEORADIUSBYMEMBER';
|
||||
import * as GEORADIUSBYMEMBERSTORE from '../commands/GEORADIUSBYMEMBERSTORE';
|
||||
import * as GEORADIUSSTORE from '../commands/GEORADIUSSTORE';
|
||||
import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH';
|
||||
import * as GEOSEARCH from '../commands/GEOSEARCH';
|
||||
import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE';
|
||||
import * as GET from '../commands/GET';
|
||||
import * as GETBIT from '../commands/GETBIT';
|
||||
import * as GETDEL from '../commands/GETDEL';
|
||||
import * as GETEX from '../commands/GETEX';
|
||||
import * as GETRANGE from '../commands/GETRANGE';
|
||||
import * as GETSET from '../commands/GETSET';
|
||||
import * as HDEL from '../commands/HDEL';
|
||||
import * as HEXISTS from '../commands/HEXISTS';
|
||||
import * as HEXPIRE from '../commands/HEXPIRE';
|
||||
import * as HEXPIREAT from '../commands/HEXPIREAT';
|
||||
import * as HEXPIRETIME from '../commands/HEXPIRETIME';
|
||||
import * as HGET from '../commands/HGET';
|
||||
import * as HGETALL from '../commands/HGETALL';
|
||||
import * as HINCRBY from '../commands/HINCRBY';
|
||||
import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
|
||||
import * as HKEYS from '../commands/HKEYS';
|
||||
import * as HLEN from '../commands/HLEN';
|
||||
import * as HMGET from '../commands/HMGET';
|
||||
import * as HPERSIST from '../commands/HPERSIST';
|
||||
import * as HPEXPIRE from '../commands/HPEXPIRE';
|
||||
import * as HPEXPIREAT from '../commands/HPEXPIREAT';
|
||||
import * as HPEXPIRETIME from '../commands/HPEXPIRETIME';
|
||||
import * as HPTTL from '../commands/HPTTL';
|
||||
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
|
||||
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
|
||||
import * as HRANDFIELD from '../commands/HRANDFIELD';
|
||||
import * as HSCAN from '../commands/HSCAN';
|
||||
import * as HSCAN_NOVALUES from '../commands/HSCAN_NOVALUES';
|
||||
import * as HSET from '../commands/HSET';
|
||||
import * as HSETNX from '../commands/HSETNX';
|
||||
import * as HSTRLEN from '../commands/HSTRLEN';
|
||||
import * as HTTL from '../commands/HTTL';
|
||||
import * as HVALS from '../commands/HVALS';
|
||||
import * as INCR from '../commands/INCR';
|
||||
import * as INCRBY from '../commands/INCRBY';
|
||||
import * as INCRBYFLOAT from '../commands/INCRBYFLOAT';
|
||||
import * as LCS_IDX_WITHMATCHLEN from '../commands/LCS_IDX_WITHMATCHLEN';
|
||||
import * as LCS_IDX from '../commands/LCS_IDX';
|
||||
import * as LCS_LEN from '../commands/LCS_LEN';
|
||||
import * as LCS from '../commands/LCS';
|
||||
import * as LINDEX from '../commands/LINDEX';
|
||||
import * as LINSERT from '../commands/LINSERT';
|
||||
import * as LLEN from '../commands/LLEN';
|
||||
import * as LMOVE from '../commands/LMOVE';
|
||||
import * as LMPOP from '../commands/LMPOP';
|
||||
import * as LPOP_COUNT from '../commands/LPOP_COUNT';
|
||||
import * as LPOP from '../commands/LPOP';
|
||||
import * as LPOS_COUNT from '../commands/LPOS_COUNT';
|
||||
import * as LPOS from '../commands/LPOS';
|
||||
import * as LPUSH from '../commands/LPUSH';
|
||||
import * as LPUSHX from '../commands/LPUSHX';
|
||||
import * as LRANGE from '../commands/LRANGE';
|
||||
import * as LREM from '../commands/LREM';
|
||||
import * as LSET from '../commands/LSET';
|
||||
import * as LTRIM from '../commands/LTRIM';
|
||||
import * as MGET from '../commands/MGET';
|
||||
import * as MIGRATE from '../commands/MIGRATE';
|
||||
import * as MSET from '../commands/MSET';
|
||||
import * as MSETNX from '../commands/MSETNX';
|
||||
import * as OBJECT_ENCODING from '../commands/OBJECT_ENCODING';
|
||||
import * as OBJECT_FREQ from '../commands/OBJECT_FREQ';
|
||||
import * as OBJECT_IDLETIME from '../commands/OBJECT_IDLETIME';
|
||||
import * as OBJECT_REFCOUNT from '../commands/OBJECT_REFCOUNT';
|
||||
import * as PERSIST from '../commands/PERSIST';
|
||||
import * as PEXPIRE from '../commands/PEXPIRE';
|
||||
import * as PEXPIREAT from '../commands/PEXPIREAT';
|
||||
import * as PEXPIRETIME from '../commands/PEXPIRETIME';
|
||||
import * as PFADD from '../commands/PFADD';
|
||||
import * as PFCOUNT from '../commands/PFCOUNT';
|
||||
import * as PFMERGE from '../commands/PFMERGE';
|
||||
import * as PSETEX from '../commands/PSETEX';
|
||||
import * as PTTL from '../commands/PTTL';
|
||||
import * as PUBLISH from '../commands/PUBLISH';
|
||||
import * as RENAME from '../commands/RENAME';
|
||||
import * as RENAMENX from '../commands/RENAMENX';
|
||||
import * as RESTORE from '../commands/RESTORE';
|
||||
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
|
||||
import * as RPOP from '../commands/RPOP';
|
||||
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
|
||||
import * as RPUSH from '../commands/RPUSH';
|
||||
import * as RPUSHX from '../commands/RPUSHX';
|
||||
import * as SADD from '../commands/SADD';
|
||||
import * as SCARD from '../commands/SCARD';
|
||||
import * as SDIFF from '../commands/SDIFF';
|
||||
import * as SDIFFSTORE from '../commands/SDIFFSTORE';
|
||||
import * as SET from '../commands/SET';
|
||||
import * as SETBIT from '../commands/SETBIT';
|
||||
import * as SETEX from '../commands/SETEX';
|
||||
import * as SETNX from '../commands/SETNX';
|
||||
import * as SETRANGE from '../commands/SETRANGE';
|
||||
import * as SINTER from '../commands/SINTER';
|
||||
import * as SINTERCARD from '../commands/SINTERCARD';
|
||||
import * as SINTERSTORE from '../commands/SINTERSTORE';
|
||||
import * as SISMEMBER from '../commands/SISMEMBER';
|
||||
import * as SMEMBERS from '../commands/SMEMBERS';
|
||||
import * as SMISMEMBER from '../commands/SMISMEMBER';
|
||||
import * as SMOVE from '../commands/SMOVE';
|
||||
import * as SORT_RO from '../commands/SORT_RO';
|
||||
import * as SORT_STORE from '../commands/SORT_STORE';
|
||||
import * as SORT from '../commands/SORT';
|
||||
import * as SPOP from '../commands/SPOP';
|
||||
import * as SPUBLISH from '../commands/SPUBLISH';
|
||||
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
|
||||
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
|
||||
import * as SREM from '../commands/SREM';
|
||||
import * as SSCAN from '../commands/SSCAN';
|
||||
import * as STRLEN from '../commands/STRLEN';
|
||||
import * as SUNION from '../commands/SUNION';
|
||||
import * as SUNIONSTORE from '../commands/SUNIONSTORE';
|
||||
import * as TOUCH from '../commands/TOUCH';
|
||||
import * as TTL from '../commands/TTL';
|
||||
import * as TYPE from '../commands/TYPE';
|
||||
import * as UNLINK from '../commands/UNLINK';
|
||||
import * as WATCH from '../commands/WATCH';
|
||||
import * as XACK from '../commands/XACK';
|
||||
import * as XADD from '../commands/XADD';
|
||||
import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID';
|
||||
import * as XAUTOCLAIM from '../commands/XAUTOCLAIM';
|
||||
import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID';
|
||||
import * as XCLAIM from '../commands/XCLAIM';
|
||||
import * as XDEL from '../commands/XDEL';
|
||||
import * as XGROUP_CREATE from '../commands/XGROUP_CREATE';
|
||||
import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER';
|
||||
import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER';
|
||||
import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY';
|
||||
import * as XGROUP_SETID from '../commands/XGROUP_SETID';
|
||||
import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS';
|
||||
import * as XINFO_GROUPS from '../commands/XINFO_GROUPS';
|
||||
import * as XINFO_STREAM from '../commands/XINFO_STREAM';
|
||||
import * as XLEN from '../commands/XLEN';
|
||||
import * as XPENDING_RANGE from '../commands/XPENDING_RANGE';
|
||||
import * as XPENDING from '../commands/XPENDING';
|
||||
import * as XRANGE from '../commands/XRANGE';
|
||||
import * as XREAD from '../commands/XREAD';
|
||||
import * as XREADGROUP from '../commands/XREADGROUP';
|
||||
import * as XREVRANGE from '../commands/XREVRANGE';
|
||||
import * as XSETID from '../commands/XSETID';
|
||||
import * as XTRIM from '../commands/XTRIM';
|
||||
import * as ZADD from '../commands/ZADD';
|
||||
import * as ZCARD from '../commands/ZCARD';
|
||||
import * as ZCOUNT from '../commands/ZCOUNT';
|
||||
import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES';
|
||||
import * as ZDIFF from '../commands/ZDIFF';
|
||||
import * as ZDIFFSTORE from '../commands/ZDIFFSTORE';
|
||||
import * as ZINCRBY from '../commands/ZINCRBY';
|
||||
import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES';
|
||||
import * as ZINTER from '../commands/ZINTER';
|
||||
import * as ZINTERCARD from '../commands/ZINTERCARD';
|
||||
import * as ZINTERSTORE from '../commands/ZINTERSTORE';
|
||||
import * as ZLEXCOUNT from '../commands/ZLEXCOUNT';
|
||||
import * as ZMPOP from '../commands/ZMPOP';
|
||||
import * as ZMSCORE from '../commands/ZMSCORE';
|
||||
import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT';
|
||||
import * as ZPOPMAX from '../commands/ZPOPMAX';
|
||||
import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT';
|
||||
import * as ZPOPMIN from '../commands/ZPOPMIN';
|
||||
import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES';
|
||||
import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT';
|
||||
import * as ZRANDMEMBER from '../commands/ZRANDMEMBER';
|
||||
import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES';
|
||||
import * as ZRANGE from '../commands/ZRANGE';
|
||||
import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX';
|
||||
import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES';
|
||||
import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE';
|
||||
import * as ZRANGESTORE from '../commands/ZRANGESTORE';
|
||||
import * as ZRANK from '../commands/ZRANK';
|
||||
import * as ZREM from '../commands/ZREM';
|
||||
import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX';
|
||||
import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK';
|
||||
import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE';
|
||||
import * as ZREVRANK from '../commands/ZREVRANK';
|
||||
import * as ZSCAN from '../commands/ZSCAN';
|
||||
import * as ZSCORE from '../commands/ZSCORE';
|
||||
import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES';
|
||||
import * as ZUNION from '../commands/ZUNION';
|
||||
import * as ZUNIONSTORE from '../commands/ZUNIONSTORE';
|
||||
|
||||
export default {
|
||||
APPEND,
|
||||
append: APPEND,
|
||||
BITCOUNT,
|
||||
bitCount: BITCOUNT,
|
||||
BITFIELD_RO,
|
||||
bitFieldRo: BITFIELD_RO,
|
||||
BITFIELD,
|
||||
bitField: BITFIELD,
|
||||
BITOP,
|
||||
bitOp: BITOP,
|
||||
BITPOS,
|
||||
bitPos: BITPOS,
|
||||
BLMOVE,
|
||||
blMove: BLMOVE,
|
||||
BLMPOP,
|
||||
blmPop: BLMPOP,
|
||||
BLPOP,
|
||||
blPop: BLPOP,
|
||||
BRPOP,
|
||||
brPop: BRPOP,
|
||||
BRPOPLPUSH,
|
||||
brPopLPush: BRPOPLPUSH,
|
||||
BZMPOP,
|
||||
bzmPop: BZMPOP,
|
||||
BZPOPMAX,
|
||||
bzPopMax: BZPOPMAX,
|
||||
BZPOPMIN,
|
||||
bzPopMin: BZPOPMIN,
|
||||
COPY,
|
||||
copy: COPY,
|
||||
DECR,
|
||||
decr: DECR,
|
||||
DECRBY,
|
||||
decrBy: DECRBY,
|
||||
DEL,
|
||||
del: DEL,
|
||||
DUMP,
|
||||
dump: DUMP,
|
||||
EVAL_RO,
|
||||
evalRo: EVAL_RO,
|
||||
EVAL,
|
||||
eval: EVAL,
|
||||
EVALSHA,
|
||||
evalSha: EVALSHA,
|
||||
EVALSHA_RO,
|
||||
evalShaRo: EVALSHA_RO,
|
||||
EXISTS,
|
||||
exists: EXISTS,
|
||||
EXPIRE,
|
||||
expire: EXPIRE,
|
||||
EXPIREAT,
|
||||
expireAt: EXPIREAT,
|
||||
EXPIRETIME,
|
||||
expireTime: EXPIRETIME,
|
||||
FCALL_RO,
|
||||
fCallRo: FCALL_RO,
|
||||
FCALL,
|
||||
fCall: FCALL,
|
||||
GEOADD,
|
||||
geoAdd: GEOADD,
|
||||
GEODIST,
|
||||
geoDist: GEODIST,
|
||||
GEOHASH,
|
||||
geoHash: GEOHASH,
|
||||
GEOPOS,
|
||||
geoPos: GEOPOS,
|
||||
GEORADIUS_RO_WITH,
|
||||
geoRadiusRoWith: GEORADIUS_RO_WITH,
|
||||
GEORADIUS_RO,
|
||||
geoRadiusRo: GEORADIUS_RO,
|
||||
GEORADIUS_WITH,
|
||||
geoRadiusWith: GEORADIUS_WITH,
|
||||
GEORADIUS,
|
||||
geoRadius: GEORADIUS,
|
||||
GEORADIUSBYMEMBER_RO_WITH,
|
||||
geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH,
|
||||
GEORADIUSBYMEMBER_RO,
|
||||
geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO,
|
||||
GEORADIUSBYMEMBER_WITH,
|
||||
geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH,
|
||||
GEORADIUSBYMEMBER,
|
||||
geoRadiusByMember: GEORADIUSBYMEMBER,
|
||||
GEORADIUSBYMEMBERSTORE,
|
||||
geoRadiusByMemberStore: GEORADIUSBYMEMBERSTORE,
|
||||
GEORADIUSSTORE,
|
||||
geoRadiusStore: GEORADIUSSTORE,
|
||||
GEOSEARCH_WITH,
|
||||
geoSearchWith: GEOSEARCH_WITH,
|
||||
GEOSEARCH,
|
||||
geoSearch: GEOSEARCH,
|
||||
GEOSEARCHSTORE,
|
||||
geoSearchStore: GEOSEARCHSTORE,
|
||||
GET,
|
||||
get: GET,
|
||||
GETBIT,
|
||||
getBit: GETBIT,
|
||||
GETDEL,
|
||||
getDel: GETDEL,
|
||||
GETEX,
|
||||
getEx: GETEX,
|
||||
GETRANGE,
|
||||
getRange: GETRANGE,
|
||||
GETSET,
|
||||
getSet: GETSET,
|
||||
HDEL,
|
||||
hDel: HDEL,
|
||||
HEXISTS,
|
||||
hExists: HEXISTS,
|
||||
HEXPIRE,
|
||||
hExpire: HEXPIRE,
|
||||
HEXPIREAT,
|
||||
hExpireAt: HEXPIREAT,
|
||||
HEXPIRETIME,
|
||||
hExpireTime: HEXPIRETIME,
|
||||
HGET,
|
||||
hGet: HGET,
|
||||
HGETALL,
|
||||
hGetAll: HGETALL,
|
||||
HINCRBY,
|
||||
hIncrBy: HINCRBY,
|
||||
HINCRBYFLOAT,
|
||||
hIncrByFloat: HINCRBYFLOAT,
|
||||
HKEYS,
|
||||
hKeys: HKEYS,
|
||||
HLEN,
|
||||
hLen: HLEN,
|
||||
HMGET,
|
||||
hmGet: HMGET,
|
||||
HPERSIST,
|
||||
hPersist: HPERSIST,
|
||||
HPEXPIRE,
|
||||
hpExpire: HPEXPIRE,
|
||||
HPEXPIREAT,
|
||||
hpExpireAt: HPEXPIREAT,
|
||||
HPEXPIRETIME,
|
||||
hpExpireTime: HPEXPIRETIME,
|
||||
HPTTL,
|
||||
hpTTL: HPTTL,
|
||||
HRANDFIELD_COUNT_WITHVALUES,
|
||||
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
|
||||
HRANDFIELD_COUNT,
|
||||
hRandFieldCount: HRANDFIELD_COUNT,
|
||||
HRANDFIELD,
|
||||
hRandField: HRANDFIELD,
|
||||
HSCAN,
|
||||
hScan: HSCAN,
|
||||
HSCAN_NOVALUES,
|
||||
hScanNoValues: HSCAN_NOVALUES,
|
||||
HSET,
|
||||
hSet: HSET,
|
||||
HSETNX,
|
||||
hSetNX: HSETNX,
|
||||
HSTRLEN,
|
||||
hStrLen: HSTRLEN,
|
||||
HTTL,
|
||||
hTTL: HTTL,
|
||||
HVALS,
|
||||
hVals: HVALS,
|
||||
INCR,
|
||||
incr: INCR,
|
||||
INCRBY,
|
||||
incrBy: INCRBY,
|
||||
INCRBYFLOAT,
|
||||
incrByFloat: INCRBYFLOAT,
|
||||
LCS_IDX_WITHMATCHLEN,
|
||||
lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN,
|
||||
LCS_IDX,
|
||||
lcsIdx: LCS_IDX,
|
||||
LCS_LEN,
|
||||
lcsLen: LCS_LEN,
|
||||
LCS,
|
||||
lcs: LCS,
|
||||
LINDEX,
|
||||
lIndex: LINDEX,
|
||||
LINSERT,
|
||||
lInsert: LINSERT,
|
||||
LLEN,
|
||||
lLen: LLEN,
|
||||
LMOVE,
|
||||
lMove: LMOVE,
|
||||
LMPOP,
|
||||
lmPop: LMPOP,
|
||||
LPOP_COUNT,
|
||||
lPopCount: LPOP_COUNT,
|
||||
LPOP,
|
||||
lPop: LPOP,
|
||||
LPOS_COUNT,
|
||||
lPosCount: LPOS_COUNT,
|
||||
LPOS,
|
||||
lPos: LPOS,
|
||||
LPUSH,
|
||||
lPush: LPUSH,
|
||||
LPUSHX,
|
||||
lPushX: LPUSHX,
|
||||
LRANGE,
|
||||
lRange: LRANGE,
|
||||
LREM,
|
||||
lRem: LREM,
|
||||
LSET,
|
||||
lSet: LSET,
|
||||
LTRIM,
|
||||
lTrim: LTRIM,
|
||||
MGET,
|
||||
mGet: MGET,
|
||||
MIGRATE,
|
||||
migrate: MIGRATE,
|
||||
MSET,
|
||||
mSet: MSET,
|
||||
MSETNX,
|
||||
mSetNX: MSETNX,
|
||||
OBJECT_ENCODING,
|
||||
objectEncoding: OBJECT_ENCODING,
|
||||
OBJECT_FREQ,
|
||||
objectFreq: OBJECT_FREQ,
|
||||
OBJECT_IDLETIME,
|
||||
objectIdleTime: OBJECT_IDLETIME,
|
||||
OBJECT_REFCOUNT,
|
||||
objectRefCount: OBJECT_REFCOUNT,
|
||||
PERSIST,
|
||||
persist: PERSIST,
|
||||
PEXPIRE,
|
||||
pExpire: PEXPIRE,
|
||||
PEXPIREAT,
|
||||
pExpireAt: PEXPIREAT,
|
||||
PEXPIRETIME,
|
||||
pExpireTime: PEXPIRETIME,
|
||||
PFADD,
|
||||
pfAdd: PFADD,
|
||||
PFCOUNT,
|
||||
pfCount: PFCOUNT,
|
||||
PFMERGE,
|
||||
pfMerge: PFMERGE,
|
||||
PSETEX,
|
||||
pSetEx: PSETEX,
|
||||
PTTL,
|
||||
pTTL: PTTL,
|
||||
PUBLISH,
|
||||
publish: PUBLISH,
|
||||
RENAME,
|
||||
rename: RENAME,
|
||||
RENAMENX,
|
||||
renameNX: RENAMENX,
|
||||
RESTORE,
|
||||
restore: RESTORE,
|
||||
RPOP_COUNT,
|
||||
rPopCount: RPOP_COUNT,
|
||||
RPOP,
|
||||
rPop: RPOP,
|
||||
RPOPLPUSH,
|
||||
rPopLPush: RPOPLPUSH,
|
||||
RPUSH,
|
||||
rPush: RPUSH,
|
||||
RPUSHX,
|
||||
rPushX: RPUSHX,
|
||||
SADD,
|
||||
sAdd: SADD,
|
||||
SCARD,
|
||||
sCard: SCARD,
|
||||
SDIFF,
|
||||
sDiff: SDIFF,
|
||||
SDIFFSTORE,
|
||||
sDiffStore: SDIFFSTORE,
|
||||
SINTER,
|
||||
sInter: SINTER,
|
||||
SINTERCARD,
|
||||
sInterCard: SINTERCARD,
|
||||
SINTERSTORE,
|
||||
sInterStore: SINTERSTORE,
|
||||
SET,
|
||||
set: SET,
|
||||
SETBIT,
|
||||
setBit: SETBIT,
|
||||
SETEX,
|
||||
setEx: SETEX,
|
||||
SETNX,
|
||||
setNX: SETNX,
|
||||
SETRANGE,
|
||||
setRange: SETRANGE,
|
||||
SISMEMBER,
|
||||
sIsMember: SISMEMBER,
|
||||
SMEMBERS,
|
||||
sMembers: SMEMBERS,
|
||||
SMISMEMBER,
|
||||
smIsMember: SMISMEMBER,
|
||||
SMOVE,
|
||||
sMove: SMOVE,
|
||||
SORT_RO,
|
||||
sortRo: SORT_RO,
|
||||
SORT_STORE,
|
||||
sortStore: SORT_STORE,
|
||||
SORT,
|
||||
sort: SORT,
|
||||
SPOP,
|
||||
sPop: SPOP,
|
||||
SPUBLISH,
|
||||
sPublish: SPUBLISH,
|
||||
SRANDMEMBER_COUNT,
|
||||
sRandMemberCount: SRANDMEMBER_COUNT,
|
||||
SRANDMEMBER,
|
||||
sRandMember: SRANDMEMBER,
|
||||
SREM,
|
||||
sRem: SREM,
|
||||
SSCAN,
|
||||
sScan: SSCAN,
|
||||
STRLEN,
|
||||
strLen: STRLEN,
|
||||
SUNION,
|
||||
sUnion: SUNION,
|
||||
SUNIONSTORE,
|
||||
sUnionStore: SUNIONSTORE,
|
||||
TOUCH,
|
||||
touch: TOUCH,
|
||||
TTL,
|
||||
ttl: TTL,
|
||||
TYPE,
|
||||
type: TYPE,
|
||||
UNLINK,
|
||||
unlink: UNLINK,
|
||||
WATCH,
|
||||
watch: WATCH,
|
||||
XACK,
|
||||
xAck: XACK,
|
||||
XADD,
|
||||
xAdd: XADD,
|
||||
XAUTOCLAIM_JUSTID,
|
||||
xAutoClaimJustId: XAUTOCLAIM_JUSTID,
|
||||
XAUTOCLAIM,
|
||||
xAutoClaim: XAUTOCLAIM,
|
||||
XCLAIM,
|
||||
xClaim: XCLAIM,
|
||||
XCLAIM_JUSTID,
|
||||
xClaimJustId: XCLAIM_JUSTID,
|
||||
XDEL,
|
||||
xDel: XDEL,
|
||||
XGROUP_CREATE,
|
||||
xGroupCreate: XGROUP_CREATE,
|
||||
XGROUP_CREATECONSUMER,
|
||||
xGroupCreateConsumer: XGROUP_CREATECONSUMER,
|
||||
XGROUP_DELCONSUMER,
|
||||
xGroupDelConsumer: XGROUP_DELCONSUMER,
|
||||
XGROUP_DESTROY,
|
||||
xGroupDestroy: XGROUP_DESTROY,
|
||||
XGROUP_SETID,
|
||||
xGroupSetId: XGROUP_SETID,
|
||||
XINFO_CONSUMERS,
|
||||
xInfoConsumers: XINFO_CONSUMERS,
|
||||
XINFO_GROUPS,
|
||||
xInfoGroups: XINFO_GROUPS,
|
||||
XINFO_STREAM,
|
||||
xInfoStream: XINFO_STREAM,
|
||||
XLEN,
|
||||
xLen: XLEN,
|
||||
XPENDING_RANGE,
|
||||
xPendingRange: XPENDING_RANGE,
|
||||
XPENDING,
|
||||
xPending: XPENDING,
|
||||
XRANGE,
|
||||
xRange: XRANGE,
|
||||
XREAD,
|
||||
xRead: XREAD,
|
||||
XREADGROUP,
|
||||
xReadGroup: XREADGROUP,
|
||||
XREVRANGE,
|
||||
xRevRange: XREVRANGE,
|
||||
XSETID,
|
||||
xSetId: XSETID,
|
||||
XTRIM,
|
||||
xTrim: XTRIM,
|
||||
ZADD,
|
||||
zAdd: ZADD,
|
||||
ZCARD,
|
||||
zCard: ZCARD,
|
||||
ZCOUNT,
|
||||
zCount: ZCOUNT,
|
||||
ZDIFF_WITHSCORES,
|
||||
zDiffWithScores: ZDIFF_WITHSCORES,
|
||||
ZDIFF,
|
||||
zDiff: ZDIFF,
|
||||
ZDIFFSTORE,
|
||||
zDiffStore: ZDIFFSTORE,
|
||||
ZINCRBY,
|
||||
zIncrBy: ZINCRBY,
|
||||
ZINTER_WITHSCORES,
|
||||
zInterWithScores: ZINTER_WITHSCORES,
|
||||
ZINTER,
|
||||
zInter: ZINTER,
|
||||
ZINTERCARD,
|
||||
zInterCard: ZINTERCARD,
|
||||
ZINTERSTORE,
|
||||
zInterStore: ZINTERSTORE,
|
||||
ZLEXCOUNT,
|
||||
zLexCount: ZLEXCOUNT,
|
||||
ZMPOP,
|
||||
zmPop: ZMPOP,
|
||||
ZMSCORE,
|
||||
zmScore: ZMSCORE,
|
||||
ZPOPMAX_COUNT,
|
||||
zPopMaxCount: ZPOPMAX_COUNT,
|
||||
ZPOPMAX,
|
||||
zPopMax: ZPOPMAX,
|
||||
ZPOPMIN_COUNT,
|
||||
zPopMinCount: ZPOPMIN_COUNT,
|
||||
ZPOPMIN,
|
||||
zPopMin: ZPOPMIN,
|
||||
ZRANDMEMBER_COUNT_WITHSCORES,
|
||||
zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
|
||||
ZRANDMEMBER_COUNT,
|
||||
zRandMemberCount: ZRANDMEMBER_COUNT,
|
||||
ZRANDMEMBER,
|
||||
zRandMember: ZRANDMEMBER,
|
||||
ZRANGE_WITHSCORES,
|
||||
zRangeWithScores: ZRANGE_WITHSCORES,
|
||||
ZRANGE,
|
||||
zRange: ZRANGE,
|
||||
ZRANGEBYLEX,
|
||||
zRangeByLex: ZRANGEBYLEX,
|
||||
ZRANGEBYSCORE_WITHSCORES,
|
||||
zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
|
||||
ZRANGEBYSCORE,
|
||||
zRangeByScore: ZRANGEBYSCORE,
|
||||
ZRANGESTORE,
|
||||
zRangeStore: ZRANGESTORE,
|
||||
ZRANK,
|
||||
zRank: ZRANK,
|
||||
ZREM,
|
||||
zRem: ZREM,
|
||||
ZREMRANGEBYLEX,
|
||||
zRemRangeByLex: ZREMRANGEBYLEX,
|
||||
ZREMRANGEBYRANK,
|
||||
zRemRangeByRank: ZREMRANGEBYRANK,
|
||||
ZREMRANGEBYSCORE,
|
||||
zRemRangeByScore: ZREMRANGEBYSCORE,
|
||||
ZREVRANK,
|
||||
zRevRank: ZREVRANK,
|
||||
ZSCAN,
|
||||
zScan: ZSCAN,
|
||||
ZSCORE,
|
||||
zScore: ZSCORE,
|
||||
ZUNION_WITHSCORES,
|
||||
zUnionWithScores: ZUNION_WITHSCORES,
|
||||
ZUNION,
|
||||
zUnion: ZUNION,
|
||||
ZUNIONSTORE,
|
||||
zUnionStore: ZUNIONSTORE
|
||||
};
|
@@ -1,389 +1,342 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||
import RedisCluster from '.';
|
||||
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
|
||||
import { commandOptions } from '../command-options';
|
||||
import { SQUARE_SCRIPT } from '../client/index.spec';
|
||||
import { RootNodesUnavailableError } from '../errors';
|
||||
import { spy } from 'sinon';
|
||||
import { promiseTimeout } from '../utils';
|
||||
import RedisClient from '../client';
|
||||
|
||||
describe('Cluster', () => {
|
||||
testUtils.testWithCluster('sendCommand', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.sendCommand(undefined, true, ['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testWithCluster('sendCommand', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.sendCommand(undefined, true, ['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('isOpen', async cluster => {
|
||||
assert.equal(cluster.isOpen, true);
|
||||
await cluster.disconnect();
|
||||
assert.equal(cluster.isOpen, false);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testWithCluster('isOpen', async cluster => {
|
||||
assert.equal(cluster.isOpen, true);
|
||||
await cluster.destroy();
|
||||
assert.equal(cluster.isOpen, false);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('connect should throw if already connected', async cluster => {
|
||||
await assert.rejects(cluster.connect());
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testWithCluster('connect should throw if already connected', async cluster => {
|
||||
await assert.rejects(cluster.connect());
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('multi', async cluster => {
|
||||
const key = 'key';
|
||||
assert.deepEqual(
|
||||
await cluster.multi()
|
||||
.set(key, 'value')
|
||||
.get(key)
|
||||
.exec(),
|
||||
['OK', 'value']
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testWithCluster('multi', async cluster => {
|
||||
const key = 'key';
|
||||
assert.deepEqual(
|
||||
await cluster.multi()
|
||||
.set(key, 'value')
|
||||
.get(key)
|
||||
.exec(),
|
||||
['OK', 'value']
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('scripts', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.square(2),
|
||||
4
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
testUtils.testWithCluster('scripts', async cluster => {
|
||||
const [, reply] = await Promise.all([
|
||||
cluster.set('key', '2'),
|
||||
cluster.square('key')
|
||||
]);
|
||||
|
||||
assert.equal(reply, 4);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw RootNodesUnavailableError', async () => {
|
||||
const cluster = RedisCluster.create({
|
||||
rootNodes: []
|
||||
});
|
||||
|
||||
it('should throw RootNodesUnavailableError', async () => {
|
||||
const cluster = RedisCluster.create({
|
||||
rootNodes: []
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
cluster.connect(),
|
||||
RootNodesUnavailableError
|
||||
);
|
||||
} catch (err) {
|
||||
await cluster.disconnect();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
cluster.connect(),
|
||||
RootNodesUnavailableError
|
||||
);
|
||||
} catch (err) {
|
||||
await cluster.disconnect();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
testUtils.testWithCluster('should handle live resharding', async cluster => {
|
||||
const slot = 12539,
|
||||
key = 'key',
|
||||
value = 'value';
|
||||
await cluster.set(key, value);
|
||||
|
||||
testUtils.testWithCluster('should handle live resharding', async cluster => {
|
||||
const slot = 12539,
|
||||
key = 'key',
|
||||
value = 'value';
|
||||
await cluster.set(key, value);
|
||||
const importing = cluster.slots[0].master,
|
||||
migrating = cluster.slots[slot].master,
|
||||
[importingClient, migratingClient] = await Promise.all([
|
||||
cluster.nodeClient(importing),
|
||||
cluster.nodeClient(migrating)
|
||||
]);
|
||||
|
||||
const importing = cluster.slots[0].master,
|
||||
migrating = cluster.slots[slot].master,
|
||||
[ importingClient, migratingClient ] = await Promise.all([
|
||||
cluster.nodeClient(importing),
|
||||
cluster.nodeClient(migrating)
|
||||
]);
|
||||
await Promise.all([
|
||||
importingClient.clusterSetSlot(slot, 'IMPORTING', migrating.id),
|
||||
migratingClient.clusterSetSlot(slot, 'MIGRATING', importing.id)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id),
|
||||
migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id)
|
||||
]);
|
||||
// should be able to get the key from the migrating node
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
|
||||
// should be able to get the key from the migrating node
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
await migratingClient.migrate(
|
||||
importing.host,
|
||||
importing.port,
|
||||
key,
|
||||
0,
|
||||
10
|
||||
);
|
||||
|
||||
await migratingClient.migrate(
|
||||
importing.host,
|
||||
importing.port,
|
||||
key,
|
||||
0,
|
||||
10
|
||||
);
|
||||
// should be able to get the key from the importing node using `ASKING`
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
|
||||
// should be able to get the key from the importing node using `ASKING`
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
await Promise.all([
|
||||
importingClient.clusterSetSlot(slot, 'NODE', importing.id),
|
||||
migratingClient.clusterSetSlot(slot, 'NODE', importing.id),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
|
||||
migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
|
||||
]);
|
||||
// should handle `MOVED` errors
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
}, {
|
||||
serverArguments: [],
|
||||
numberOfMasters: 2
|
||||
});
|
||||
|
||||
// should handle `MOVED` errors
|
||||
assert.equal(
|
||||
await cluster.get(key),
|
||||
value
|
||||
);
|
||||
}, {
|
||||
serverArguments: [],
|
||||
numberOfMasters: 2
|
||||
});
|
||||
testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
|
||||
const totalNodes = cluster.masters.length + cluster.replicas.length,
|
||||
ids = new Set<string>();
|
||||
for (let i = 0; i < totalNodes; i++) {
|
||||
ids.add(cluster.getRandomNode().id);
|
||||
}
|
||||
|
||||
testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
|
||||
const totalNodes = cluster.masters.length + cluster.replicas.length,
|
||||
ids = new Set<string>();
|
||||
for (let i = 0; i < totalNodes; i++) {
|
||||
ids.add(cluster.getRandomNode().id);
|
||||
}
|
||||
|
||||
assert.equal(ids.size, totalNodes);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
assert.equal(ids.size, totalNodes);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
|
||||
testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
|
||||
const totalNodes = 1 + cluster.slots[0].replicas!.length,
|
||||
ids = new Set<string>();
|
||||
for (let i = 0; i < totalNodes; i++) {
|
||||
ids.add(cluster.getSlotRandomNode(0).id);
|
||||
}
|
||||
|
||||
assert.equal(ids.size, totalNodes);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
|
||||
const totalNodes = 1 + cluster.slots[0].replicas!.length,
|
||||
ids = new Set<string>();
|
||||
for (let i = 0; i < totalNodes; i++) {
|
||||
ids.add(cluster.getSlotRandomNode(0).id);
|
||||
}
|
||||
|
||||
testUtils.testWithCluster('cluster topology', async cluster => {
|
||||
assert.equal(cluster.slots.length, 16384);
|
||||
const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
|
||||
assert.equal(cluster.shards.length, numberOfMasters);
|
||||
assert.equal(cluster.masters.length, numberOfMasters);
|
||||
assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
|
||||
assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
assert.equal(ids.size, totalNodes);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
|
||||
testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||
const masters = cluster.getMasters();
|
||||
assert.ok(Array.isArray(masters));
|
||||
for (const master of masters) {
|
||||
assert.equal(typeof master.id, 'string');
|
||||
assert.ok(master.client instanceof RedisClient);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: undefined // reset to default
|
||||
}
|
||||
});
|
||||
testUtils.testWithCluster('cluster topology', async cluster => {
|
||||
assert.equal(cluster.slots.length, 16384);
|
||||
const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
|
||||
assert.equal(cluster.masters.length, numberOfMasters);
|
||||
assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
|
||||
assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
|
||||
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||
|
||||
testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||
const master = cluster.getSlotMaster(0);
|
||||
assert.equal(typeof master.id, 'string');
|
||||
testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||
const masters = cluster.getMasters();
|
||||
assert.ok(Array.isArray(masters));
|
||||
for (const master of masters) {
|
||||
assert.equal(typeof master.id, 'string');
|
||||
assert.ok(master.client instanceof RedisClient);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: undefined // reset to default
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||
const master = cluster.getSlotMaster(0);
|
||||
assert.equal(typeof master.id, 'string');
|
||||
assert.ok(master.client instanceof RedisClient);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: undefined // reset to default
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
|
||||
await assert.rejects(cluster.mGet(['a', 'b']));
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
describe('minimizeConnections', () => {
|
||||
testUtils.testWithCluster('false', async cluster => {
|
||||
for (const master of cluster.masters) {
|
||||
assert.ok(master.client instanceof RedisClient);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: undefined // reset to default
|
||||
}
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: false
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
|
||||
await assert.rejects(cluster.mGet(['a', 'b']));
|
||||
testUtils.testWithCluster('true', async cluster => {
|
||||
for (const master of cluster.masters) {
|
||||
assert.equal(master.client, undefined);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PubSub', () => {
|
||||
testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
|
||||
await cluster.subscribe('channel', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await cluster.unsubscribe('channel', listener);
|
||||
|
||||
assert.equal(cluster.pubSubNode, undefined);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => {
|
||||
// 'a' and 'b' hash to different cluster slots (see previous unit test)
|
||||
// -> maxCommandRedirections 0: rejects on MOVED/ASK reply
|
||||
await cluster.set(commandOptions({ isolated: true }), 'a', '1'),
|
||||
await cluster.set(commandOptions({ isolated: true }), 'b', '2'),
|
||||
testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
|
||||
assert.equal(await cluster.get('a'), '1');
|
||||
assert.equal(await cluster.get('b'), '2');
|
||||
await cluster.pSubscribe('channe*', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await cluster.pUnsubscribe('channe*', listener);
|
||||
|
||||
assert.equal(cluster.pubSubNode, undefined);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
|
||||
const listener = spy();
|
||||
await cluster.subscribe('channel', listener);
|
||||
|
||||
assert.ok(cluster.pubSubNode);
|
||||
const [migrating, importing] = cluster.masters[0].address === cluster.pubSubNode.address ?
|
||||
cluster.masters :
|
||||
[cluster.masters[1], cluster.masters[0]],
|
||||
[migratingClient, importingClient] = await Promise.all([
|
||||
cluster.nodeClient(migrating),
|
||||
cluster.nodeClient(importing)
|
||||
]);
|
||||
|
||||
const range = cluster.slots[0].master === migrating ? {
|
||||
key: 'bar', // 5061
|
||||
start: 0,
|
||||
end: 8191
|
||||
} : {
|
||||
key: 'foo', // 12182
|
||||
start: 8192,
|
||||
end: 16383
|
||||
};
|
||||
|
||||
// TODO: is there a better way to migrate slots without causing CLUSTERDOWN?
|
||||
const promises: Array<Promise<unknown>> = [];
|
||||
for (let i = range.start; i <= range.end; i++) {
|
||||
promises.push(
|
||||
migratingClient.clusterSetSlot(i, 'NODE', importing.id),
|
||||
importingClient.clusterSetSlot(i, 'NODE', importing.id)
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// make sure to cause `MOVED` error
|
||||
await cluster.get(range.key);
|
||||
|
||||
await Promise.all([
|
||||
cluster.publish('channel', 'message'),
|
||||
waitTillBeenCalled(listener)
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
maxCommandRedirections: 0
|
||||
}
|
||||
serverArguments: [],
|
||||
numberOfMasters: 2,
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
|
||||
describe('minimizeConnections', () => {
|
||||
testUtils.testWithCluster('false', async cluster => {
|
||||
for (const master of cluster.masters) {
|
||||
assert.ok(master.client instanceof RedisClient);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: false
|
||||
}
|
||||
});
|
||||
testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
|
||||
testUtils.testWithCluster('true', async cluster => {
|
||||
for (const master of cluster.masters) {
|
||||
assert.equal(master.client, undefined);
|
||||
}
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
minimizeConnections: true
|
||||
}
|
||||
});
|
||||
await cluster.sSubscribe('channel', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.sPublish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await cluster.sUnsubscribe('channel', listener);
|
||||
|
||||
// 10328 is the slot of `channel`
|
||||
assert.equal(cluster.slots[10328].master.pubSub, undefined);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
|
||||
describe('PubSub', () => {
|
||||
testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
|
||||
const SLOT = 10328,
|
||||
migrating = cluster.slots[SLOT].master,
|
||||
importing = cluster.masters.find(master => master !== migrating)!,
|
||||
[migratingClient, importingClient] = await Promise.all([
|
||||
cluster.nodeClient(migrating),
|
||||
cluster.nodeClient(importing)
|
||||
]);
|
||||
|
||||
await cluster.subscribe('channel', listener);
|
||||
await Promise.all([
|
||||
migratingClient.clusterDelSlots(SLOT),
|
||||
importingClient.clusterDelSlots(SLOT),
|
||||
importingClient.clusterAddSlots(SLOT),
|
||||
// cause "topology refresh" on both nodes
|
||||
migratingClient.clusterSetSlot(SLOT, 'NODE', importing.id),
|
||||
importingClient.clusterSetSlot(SLOT, 'NODE', importing.id)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
const listener = spy();
|
||||
|
||||
await cluster.unsubscribe('channel', listener);
|
||||
// will trigger `MOVED` error
|
||||
await cluster.sSubscribe('channel', listener);
|
||||
|
||||
assert.equal(cluster.pubSubNode, undefined);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('concurrent UNSUBSCRIBE does not throw an error (#2685)', async cluster => {
|
||||
const listener = spy();
|
||||
await Promise.all([
|
||||
cluster.subscribe('1', listener),
|
||||
cluster.subscribe('2', listener)
|
||||
]);
|
||||
await Promise.all([
|
||||
cluster.unsubscribe('1', listener),
|
||||
cluster.unsubscribe('2', listener)
|
||||
]);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.sPublish('channel', 'message')
|
||||
]);
|
||||
|
||||
testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
|
||||
await cluster.pSubscribe('channe*', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await cluster.pUnsubscribe('channe*', listener);
|
||||
|
||||
assert.equal(cluster.pubSubNode, undefined);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
|
||||
const listener = spy();
|
||||
await cluster.subscribe('channel', listener);
|
||||
|
||||
assert.ok(cluster.pubSubNode);
|
||||
const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ?
|
||||
cluster.masters :
|
||||
[cluster.masters[1], cluster.masters[0]],
|
||||
[ migratingClient, importingClient ] = await Promise.all([
|
||||
cluster.nodeClient(migrating),
|
||||
cluster.nodeClient(importing)
|
||||
]);
|
||||
|
||||
const range = cluster.slots[0].master === migrating ? {
|
||||
key: 'bar', // 5061
|
||||
start: 0,
|
||||
end: 8191
|
||||
} : {
|
||||
key: 'foo', // 12182
|
||||
start: 8192,
|
||||
end: 16383
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
migratingClient.clusterDelSlotsRange(range),
|
||||
importingClient.clusterDelSlotsRange(range),
|
||||
importingClient.clusterAddSlotsRange(range)
|
||||
]);
|
||||
|
||||
// wait for migrating node to be notified about the new topology
|
||||
while ((await migratingClient.clusterInfo()).state !== 'ok') {
|
||||
await promiseTimeout(50);
|
||||
}
|
||||
|
||||
// make sure to cause `MOVED` error
|
||||
await cluster.get(range.key);
|
||||
|
||||
await Promise.all([
|
||||
cluster.publish('channel', 'message'),
|
||||
waitTillBeenCalled(listener)
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
}, {
|
||||
serverArguments: [],
|
||||
numberOfMasters: 2,
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
|
||||
const listener = spy();
|
||||
|
||||
await cluster.sSubscribe('channel', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.sPublish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await cluster.sUnsubscribe('channel', listener);
|
||||
|
||||
// 10328 is the slot of `channel`
|
||||
assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('concurrent SUNSUBCRIBE does not throw an error (#2685)', async cluster => {
|
||||
const listener = spy();
|
||||
await Promise.all([
|
||||
await cluster.sSubscribe('1', listener),
|
||||
await cluster.sSubscribe('2', listener)
|
||||
]);
|
||||
await Promise.all([
|
||||
cluster.sUnsubscribe('1', listener),
|
||||
cluster.sUnsubscribe('2', listener)
|
||||
]);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
|
||||
const SLOT = 10328,
|
||||
migrating = cluster.slots[SLOT].master,
|
||||
importing = cluster.masters.find(master => master !== migrating)!,
|
||||
[ migratingClient, importingClient ] = await Promise.all([
|
||||
cluster.nodeClient(migrating),
|
||||
cluster.nodeClient(importing)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
migratingClient.clusterDelSlots(SLOT),
|
||||
importingClient.clusterDelSlots(SLOT),
|
||||
importingClient.clusterAddSlots(SLOT)
|
||||
]);
|
||||
|
||||
// wait for migrating node to be notified about the new topology
|
||||
while ((await migratingClient.clusterInfo()).state !== 'ok') {
|
||||
await promiseTimeout(50);
|
||||
}
|
||||
|
||||
const listener = spy();
|
||||
|
||||
// will trigger `MOVED` error
|
||||
await cluster.sSubscribe('channel', listener);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(listener),
|
||||
cluster.sPublish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
}, {
|
||||
serverArguments: [],
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||
}, {
|
||||
serverArguments: [],
|
||||
minimumDockerVersion: [7]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,141 +1,262 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
|
||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { attachCommands, attachExtensions } from '../commander';
|
||||
import COMMANDS from '../commands';
|
||||
import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping, RedisArgument } from '../RESP/types';
|
||||
import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
|
||||
import RedisCluster from '.';
|
||||
|
||||
type RedisClusterMultiCommandSignature<
|
||||
C extends RedisCommand,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
|
||||
type CommandSignature<
|
||||
REPLIES extends Array<unknown>,
|
||||
C extends Command,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<
|
||||
[...REPLIES, ReplyWithTypeMapping<CommandReply<C, RESP>, TYPE_MAPPING>],
|
||||
M,
|
||||
F,
|
||||
S,
|
||||
RESP,
|
||||
TYPE_MAPPING
|
||||
>;
|
||||
|
||||
type WithCommands<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
|
||||
[P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
|
||||
type WithModules<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof M as ExcludeMappedString<P>]: {
|
||||
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
|
||||
};
|
||||
[P in keyof M]: {
|
||||
[C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithFunctions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof F as ExcludeMappedString<P>]: {
|
||||
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
|
||||
};
|
||||
[L in keyof F]: {
|
||||
[C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
REPLIES extends Array<unknown>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = {
|
||||
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
|
||||
[P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, TYPE_MAPPING>;
|
||||
};
|
||||
|
||||
export type RedisClusterMultiCommandType<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
|
||||
REPLIES extends Array<any>,
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions,
|
||||
TYPE_MAPPING extends TypeMapping
|
||||
> = (
|
||||
RedisClusterMultiCommand<REPLIES> &
|
||||
WithCommands<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithModules<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithFunctions<REPLIES, M, F, S, RESP, TYPE_MAPPING> &
|
||||
WithScripts<REPLIES, M, F, S, RESP, TYPE_MAPPING>
|
||||
);
|
||||
|
||||
export type InstantiableRedisClusterMultiCommandType<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
> = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
|
||||
export type ClusterMultiExecute = (
|
||||
firstKey: RedisArgument | undefined,
|
||||
isReadonly: boolean | undefined,
|
||||
commands: Array<RedisMultiQueuedCommand>
|
||||
) => Promise<Array<unknown>>;
|
||||
|
||||
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||
export default class RedisClusterMultiCommand<REPLIES = []> {
|
||||
static #createCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return function (this: RedisClusterMultiCommand, ...args: Array<unknown>) {
|
||||
const redisArgs = command.transformArguments(...args);
|
||||
const firstKey = RedisCluster.extractFirstKey(
|
||||
command,
|
||||
args,
|
||||
redisArgs
|
||||
);
|
||||
return this.addCommand(
|
||||
firstKey,
|
||||
command.IS_READ_ONLY,
|
||||
redisArgs,
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default class RedisClusterMultiCommand {
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executor: RedisClusterMultiExecutor;
|
||||
#firstKey: RedisCommandArgument | undefined;
|
||||
|
||||
static extend<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts
|
||||
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
|
||||
return attachExtensions({
|
||||
BaseClass: RedisClusterMultiCommand,
|
||||
modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
|
||||
modules: extensions?.modules,
|
||||
functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
|
||||
functions: extensions?.functions,
|
||||
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
|
||||
scripts: extensions?.scripts
|
||||
});
|
||||
}
|
||||
|
||||
constructor(executor: RedisClusterMultiExecutor, firstKey?: RedisCommandArgument) {
|
||||
this.#executor = executor;
|
||||
this.#firstKey = firstKey;
|
||||
}
|
||||
|
||||
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
||||
const transformedArguments = command.transformArguments(...args);
|
||||
this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
|
||||
return this.addCommand(undefined, transformedArguments, command.transformReply);
|
||||
}
|
||||
|
||||
addCommand(
|
||||
firstKey: RedisCommandArgument | undefined,
|
||||
args: RedisCommandArguments,
|
||||
transformReply?: RedisCommand['transformReply']
|
||||
): this {
|
||||
this.#firstKey ??= firstKey;
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
|
||||
const transformedArguments = this.#multi.addFunction(name, fn, args);
|
||||
this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
|
||||
return this;
|
||||
}
|
||||
|
||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||
const transformedArguments = this.#multi.addScript(script, args);
|
||||
this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
|
||||
return this;
|
||||
}
|
||||
|
||||
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
||||
if (execAsPipeline) {
|
||||
return this.execAsPipeline();
|
||||
}
|
||||
|
||||
return this.#multi.handleExecReplies(
|
||||
await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
|
||||
static #createModuleCommand(command: Command, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(command, resp);
|
||||
return function (this: { _self: RedisClusterMultiCommand }, ...args: Array<unknown>) {
|
||||
const redisArgs = command.transformArguments(...args),
|
||||
firstKey = RedisCluster.extractFirstKey(
|
||||
command,
|
||||
args,
|
||||
redisArgs
|
||||
);
|
||||
}
|
||||
return this._self.addCommand(
|
||||
firstKey,
|
||||
command.IS_READ_ONLY,
|
||||
redisArgs,
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
EXEC = this.exec;
|
||||
static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
|
||||
const prefix = functionArgumentsPrefix(name, fn),
|
||||
transformReply = getTransformReply(fn, resp);
|
||||
return function (this: { _self: RedisClusterMultiCommand }, ...args: Array<unknown>) {
|
||||
const fnArgs = fn.transformArguments(...args);
|
||||
const redisArgs: CommandArguments = prefix.concat(fnArgs);
|
||||
const firstKey = RedisCluster.extractFirstKey(
|
||||
fn,
|
||||
args,
|
||||
fnArgs
|
||||
);
|
||||
redisArgs.preserve = fnArgs.preserve;
|
||||
return this._self.addCommand(
|
||||
firstKey,
|
||||
fn.IS_READ_ONLY,
|
||||
redisArgs,
|
||||
transformReply
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executor(this.#multi.queue, this.#firstKey)
|
||||
);
|
||||
}
|
||||
static #createScriptCommand(script: RedisScript, resp: RespVersions) {
|
||||
const transformReply = getTransformReply(script, resp);
|
||||
return function (this: RedisClusterMultiCommand, ...args: Array<unknown>) {
|
||||
const scriptArgs = script.transformArguments(...args);
|
||||
this.#setState(
|
||||
RedisCluster.extractFirstKey(
|
||||
script,
|
||||
args,
|
||||
scriptArgs
|
||||
),
|
||||
script.IS_READ_ONLY
|
||||
);
|
||||
this.#multi.addScript(
|
||||
script,
|
||||
scriptArgs,
|
||||
transformReply
|
||||
);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
static extend<
|
||||
M extends RedisModules = Record<string, never>,
|
||||
F extends RedisFunctions = Record<string, never>,
|
||||
S extends RedisScripts = Record<string, never>,
|
||||
RESP extends RespVersions = 2
|
||||
>(config?: CommanderConfig<M, F, S, RESP>) {
|
||||
return attachConfig({
|
||||
BaseClass: RedisClusterMultiCommand,
|
||||
commands: COMMANDS,
|
||||
createCommand: RedisClusterMultiCommand.#createCommand,
|
||||
createModuleCommand: RedisClusterMultiCommand.#createModuleCommand,
|
||||
createFunctionCommand: RedisClusterMultiCommand.#createFunctionCommand,
|
||||
createScriptCommand: RedisClusterMultiCommand.#createScriptCommand,
|
||||
config
|
||||
});
|
||||
}
|
||||
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executeMulti: ClusterMultiExecute;
|
||||
readonly #executePipeline: ClusterMultiExecute;
|
||||
#firstKey: RedisArgument | undefined;
|
||||
#isReadonly: boolean | undefined = true;
|
||||
readonly #typeMapping?: TypeMapping;
|
||||
|
||||
constructor(
|
||||
executeMulti: ClusterMultiExecute,
|
||||
executePipeline: ClusterMultiExecute,
|
||||
routing: RedisArgument | undefined,
|
||||
typeMapping?: TypeMapping
|
||||
) {
|
||||
this.#executeMulti = executeMulti;
|
||||
this.#executePipeline = executePipeline;
|
||||
this.#firstKey = routing;
|
||||
this.#typeMapping = typeMapping;
|
||||
}
|
||||
|
||||
#setState(
|
||||
firstKey: RedisArgument | undefined,
|
||||
isReadonly: boolean | undefined,
|
||||
) {
|
||||
this.#firstKey ??= firstKey;
|
||||
this.#isReadonly &&= isReadonly;
|
||||
}
|
||||
|
||||
addCommand(
|
||||
firstKey: RedisArgument | undefined,
|
||||
isReadonly: boolean | undefined,
|
||||
args: CommandArguments,
|
||||
transformReply?: TransformReply
|
||||
) {
|
||||
this.#setState(firstKey, isReadonly);
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false) {
|
||||
if (execAsPipeline) return this.execAsPipeline<T>();
|
||||
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executeMulti(
|
||||
this.#firstKey,
|
||||
this.#isReadonly,
|
||||
this.#multi.queue
|
||||
),
|
||||
this.#typeMapping
|
||||
) as MultiReplyType<T, REPLIES>;
|
||||
}
|
||||
|
||||
EXEC = this.exec;
|
||||
|
||||
execTyped(execAsPipeline = false) {
|
||||
return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
|
||||
}
|
||||
|
||||
async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>() {
|
||||
if (this.#multi.queue.length === 0) return [] as MultiReplyType<T, REPLIES>;
|
||||
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executePipeline(
|
||||
this.#firstKey,
|
||||
this.#isReadonly,
|
||||
this.#multi.queue
|
||||
),
|
||||
this.#typeMapping
|
||||
) as MultiReplyType<T, REPLIES>;
|
||||
}
|
||||
|
||||
execAsPipelineTyped() {
|
||||
return this.execAsPipeline<MULTI_REPLY['TYPED']>();
|
||||
}
|
||||
}
|
||||
|
||||
attachCommands({
|
||||
BaseClass: RedisClusterMultiCommand,
|
||||
commands: COMMANDS,
|
||||
executor: RedisClusterMultiCommand.prototype.commandsExecutor
|
||||
});
|
||||
|
@@ -1,14 +0,0 @@
|
||||
const symbol = Symbol('Command Options');
|
||||
|
||||
export type CommandOptions<T> = T & {
|
||||
readonly [symbol]: true;
|
||||
};
|
||||
|
||||
export function commandOptions<T>(options: T): CommandOptions<T> {
|
||||
(options as any)[symbol] = true;
|
||||
return options as CommandOptions<T>;
|
||||
}
|
||||
|
||||
export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
|
||||
return options?.[symbol] === true;
|
||||
}
|
@@ -1,165 +1,124 @@
|
||||
import { Command, CommanderConfig, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions } from './RESP/types';
|
||||
|
||||
import { ClientCommandOptions } from './client';
|
||||
import { CommandOptions, isCommandOptions } from './command-options';
|
||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands';
|
||||
|
||||
type Instantiable<T = any> = new (...args: Array<any>) => T;
|
||||
|
||||
type CommandsExecutor<C extends RedisCommand = RedisCommand> =
|
||||
(command: C, args: Array<unknown>, name: string) => unknown;
|
||||
|
||||
interface AttachCommandsConfig<C extends RedisCommand> {
|
||||
BaseClass: Instantiable;
|
||||
commands: Record<string, C>;
|
||||
executor: CommandsExecutor<C>;
|
||||
interface AttachConfigOptions<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions
|
||||
> {
|
||||
BaseClass: new (...args: any) => any;
|
||||
commands: RedisCommands;
|
||||
createCommand(command: Command, resp: RespVersions): (...args: any) => any;
|
||||
createModuleCommand(command: Command, resp: RespVersions): (...args: any) => any;
|
||||
createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions): (...args: any) => any;
|
||||
createScriptCommand(script: RedisScript, resp: RespVersions): (...args: any) => any;
|
||||
config?: CommanderConfig<M, F, S, RESP>;
|
||||
}
|
||||
|
||||
export function attachCommands<C extends RedisCommand>({
|
||||
BaseClass,
|
||||
commands,
|
||||
executor
|
||||
}: AttachCommandsConfig<C>): void {
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
|
||||
return executor.call(this, command, args, name);
|
||||
};
|
||||
}
|
||||
/* FIXME: better error message / link */
|
||||
function throwResp3SearchModuleUnstableError() {
|
||||
throw new Error('Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance');
|
||||
}
|
||||
|
||||
interface AttachExtensionsConfig<T extends Instantiable = Instantiable> {
|
||||
BaseClass: T;
|
||||
modulesExecutor: CommandsExecutor;
|
||||
modules?: RedisModules;
|
||||
functionsExecutor: CommandsExecutor<RedisFunction>;
|
||||
functions?: RedisFunctions;
|
||||
scriptsExecutor: CommandsExecutor<RedisScript>;
|
||||
scripts?: RedisScripts;
|
||||
}
|
||||
export function attachConfig<
|
||||
M extends RedisModules,
|
||||
F extends RedisFunctions,
|
||||
S extends RedisScripts,
|
||||
RESP extends RespVersions
|
||||
>({
|
||||
BaseClass,
|
||||
commands,
|
||||
createCommand,
|
||||
createModuleCommand,
|
||||
createFunctionCommand,
|
||||
createScriptCommand,
|
||||
config
|
||||
}: AttachConfigOptions<M, F, S, RESP>) {
|
||||
const RESP = config?.RESP ?? 2,
|
||||
Class: any = class extends BaseClass {};
|
||||
|
||||
export function attachExtensions(config: AttachExtensionsConfig): any {
|
||||
let Commander;
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
Class.prototype[name] = createCommand(command, RESP);
|
||||
}
|
||||
|
||||
if (config.modules) {
|
||||
Commander = attachWithNamespaces({
|
||||
BaseClass: config.BaseClass,
|
||||
namespaces: config.modules,
|
||||
executor: config.modulesExecutor
|
||||
});
|
||||
}
|
||||
|
||||
if (config.functions) {
|
||||
Commander = attachWithNamespaces({
|
||||
BaseClass: Commander ?? config.BaseClass,
|
||||
namespaces: config.functions,
|
||||
executor: config.functionsExecutor
|
||||
});
|
||||
}
|
||||
|
||||
if (config.scripts) {
|
||||
Commander ??= class extends config.BaseClass {};
|
||||
attachCommands({
|
||||
BaseClass: Commander,
|
||||
commands: config.scripts,
|
||||
executor: config.scriptsExecutor
|
||||
});
|
||||
}
|
||||
|
||||
return Commander ?? config.BaseClass;
|
||||
}
|
||||
|
||||
interface AttachWithNamespacesConfig<C extends RedisCommand> {
|
||||
BaseClass: Instantiable;
|
||||
namespaces: Record<string, Record<string, C>>;
|
||||
executor: CommandsExecutor<C>;
|
||||
}
|
||||
|
||||
function attachWithNamespaces<C extends RedisCommand>({
|
||||
BaseClass,
|
||||
namespaces,
|
||||
executor
|
||||
}: AttachWithNamespacesConfig<C>): any {
|
||||
const Commander = class extends BaseClass {
|
||||
constructor(...args: Array<any>) {
|
||||
super(...args);
|
||||
|
||||
for (const namespace of Object.keys(namespaces)) {
|
||||
this[namespace] = Object.create(this[namespace], {
|
||||
self: {
|
||||
value: this
|
||||
}
|
||||
});
|
||||
}
|
||||
if (config?.modules) {
|
||||
for (const [moduleName, module] of Object.entries(config.modules)) {
|
||||
const fns = Object.create(null);
|
||||
for (const [name, command] of Object.entries(module)) {
|
||||
if (config.RESP == 3 && command.unstableResp3 && !config.unstableResp3) {
|
||||
fns[name] = throwResp3SearchModuleUnstableError;
|
||||
} else {
|
||||
fns[name] = createModuleCommand(command, RESP);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (const [namespace, commands] of Object.entries(namespaces)) {
|
||||
Commander.prototype[namespace] = {};
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
Commander.prototype[namespace][name] = function (...args: Array<unknown>): unknown {
|
||||
return executor.call(this.self, command, args, name);
|
||||
};
|
||||
}
|
||||
attachNamespace(Class.prototype, moduleName, fns);
|
||||
}
|
||||
}
|
||||
|
||||
return Commander;
|
||||
}
|
||||
if (config?.functions) {
|
||||
for (const [library, commands] of Object.entries(config.functions)) {
|
||||
const fns = Object.create(null);
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
fns[name] = createFunctionCommand(name, command, RESP);
|
||||
}
|
||||
|
||||
export function transformCommandArguments<T = ClientCommandOptions>(
|
||||
command: RedisCommand,
|
||||
args: Array<unknown>
|
||||
): {
|
||||
jsArgs: Array<unknown>;
|
||||
args: RedisCommandArguments;
|
||||
options: CommandOptions<T> | undefined;
|
||||
} {
|
||||
let options;
|
||||
if (isCommandOptions<T>(args[0])) {
|
||||
options = args[0];
|
||||
args = args.slice(1);
|
||||
attachNamespace(Class.prototype, library, fns);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
jsArgs: args,
|
||||
args: command.transformArguments(...args),
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
export function transformLegacyCommandArguments(args: Array<any>): Array<any> {
|
||||
return args.flat().map(arg => {
|
||||
return typeof arg === 'number' || arg instanceof Date ?
|
||||
arg.toString() :
|
||||
arg;
|
||||
});
|
||||
}
|
||||
|
||||
export function transformCommandReply<C extends RedisCommand>(
|
||||
command: C,
|
||||
rawReply: unknown,
|
||||
preserved: unknown
|
||||
): RedisCommandReply<C> {
|
||||
if (!command.transformReply) {
|
||||
return rawReply as RedisCommandReply<C>;
|
||||
if (config?.scripts) {
|
||||
for (const [name, script] of Object.entries(config.scripts)) {
|
||||
Class.prototype[name] = createScriptCommand(script, RESP);
|
||||
}
|
||||
}
|
||||
|
||||
return command.transformReply(rawReply, preserved);
|
||||
return Class;
|
||||
}
|
||||
|
||||
export function fCallArguments(
|
||||
name: RedisCommandArgument,
|
||||
fn: RedisFunction,
|
||||
args: RedisCommandArguments
|
||||
): RedisCommandArguments {
|
||||
const actualArgs: RedisCommandArguments = [
|
||||
fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
|
||||
name
|
||||
];
|
||||
|
||||
if (fn.NUMBER_OF_KEYS !== undefined) {
|
||||
actualArgs.push(fn.NUMBER_OF_KEYS.toString());
|
||||
function attachNamespace(prototype: any, name: PropertyKey, fns: any) {
|
||||
Object.defineProperty(prototype, name, {
|
||||
get() {
|
||||
const value = Object.create(fns);
|
||||
value._self = this;
|
||||
Object.defineProperty(this, name, { value });
|
||||
return value;
|
||||
}
|
||||
|
||||
actualArgs.push(...args);
|
||||
|
||||
return actualArgs;
|
||||
});
|
||||
}
|
||||
|
||||
export function getTransformReply(command: Command, resp: RespVersions) {
|
||||
switch (typeof command.transformReply) {
|
||||
case 'function':
|
||||
return command.transformReply;
|
||||
|
||||
case 'object':
|
||||
return command.transformReply[resp];
|
||||
}
|
||||
}
|
||||
|
||||
export function functionArgumentsPrefix(name: string, fn: RedisFunction) {
|
||||
const prefix: Array<string | Buffer> = [
|
||||
fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
|
||||
name
|
||||
];
|
||||
|
||||
if (fn.NUMBER_OF_KEYS !== undefined) {
|
||||
prefix.push(fn.NUMBER_OF_KEYS.toString());
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
export function scriptArgumentsPrefix(script: RedisScript) {
|
||||
const prefix: Array<string | Buffer> = [
|
||||
script.IS_READ_ONLY ? 'EVALSHA_RO' : 'EVALSHA',
|
||||
script.SHA1
|
||||
];
|
||||
|
||||
if (script.NUMBER_OF_KEYS !== undefined) {
|
||||
prefix.push(script.NUMBER_OF_KEYS.toString());
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
@@ -1,23 +1,31 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_CAT';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import ACL_CAT from './ACL_CAT';
|
||||
|
||||
describe('ACL CAT', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'CAT']
|
||||
);
|
||||
});
|
||||
|
||||
it('with categoryName', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('dangerous'),
|
||||
['ACL', 'CAT', 'dangerous']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
ACL_CAT.transformArguments(),
|
||||
['ACL', 'CAT']
|
||||
);
|
||||
});
|
||||
|
||||
it('with categoryName', () => {
|
||||
assert.deepEqual(
|
||||
ACL_CAT.transformArguments('dangerous'),
|
||||
['ACL', 'CAT', 'dangerous']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclCat', async client => {
|
||||
const categories = await client.aclCat();
|
||||
assert.ok(Array.isArray(categories));
|
||||
for (const category of categories) {
|
||||
assert.equal(typeof category, 'string');
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(categoryName?: RedisCommandArgument): RedisCommandArguments {
|
||||
const args: RedisCommandArguments = ['ACL', 'CAT'];
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(categoryName?: RedisArgument) {
|
||||
const args: Array<RedisArgument> = ['ACL', 'CAT'];
|
||||
|
||||
if (categoryName) {
|
||||
args.push(categoryName);
|
||||
args.push(categoryName);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<RedisCommandArgument>;
|
||||
},
|
||||
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,30 +1,30 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_DELUSER';
|
||||
import ACL_DELUSER from './ACL_DELUSER';
|
||||
|
||||
describe('ACL DELUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username'),
|
||||
['ACL', 'DELUSER', 'username']
|
||||
);
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['1', '2']),
|
||||
['ACL', 'DELUSER', '1', '2']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
ACL_DELUSER.transformArguments('username'),
|
||||
['ACL', 'DELUSER', 'username']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclDelUser', async client => {
|
||||
assert.equal(
|
||||
await client.aclDelUser('dosenotexists'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
ACL_DELUSER.transformArguments(['1', '2']),
|
||||
['ACL', 'DELUSER', '1', '2']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclDelUser', async client => {
|
||||
assert.equal(
|
||||
typeof await client.aclDelUser('user'),
|
||||
'number'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
import { NumberReply, Command } from '../RESP/types';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
|
||||
|
||||
export function transformArguments(
|
||||
username: RedisCommandArgument | Array<RedisCommandArgument>
|
||||
): RedisCommandArguments {
|
||||
return pushVerdictArguments(['ACL', 'DELUSER'], username);
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(username: RedisVariadicArgument) {
|
||||
return pushVariadicArguments(['ACL', 'DELUSER'], username);
|
||||
},
|
||||
transformReply: undefined as unknown as () => NumberReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_DRYRUN';
|
||||
import ACL_DRYRUN from './ACL_DRYRUN';
|
||||
|
||||
describe('ACL DRYRUN', () => {
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('default', ['GET', 'key']),
|
||||
['ACL', 'DRYRUN', 'default', 'GET', 'key']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_DRYRUN.transformArguments('default', ['GET', 'key']),
|
||||
['ACL', 'DRYRUN', 'default', 'GET', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclDryRun', async client => {
|
||||
assert.equal(
|
||||
await client.aclDryRun('default', ['GET', 'key']),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testWithClient('client.aclDryRun', async client => {
|
||||
assert.equal(
|
||||
await client.aclDryRun('default', ['GET', 'key']),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,18 +1,16 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(
|
||||
username: RedisCommandArgument,
|
||||
command: Array<RedisCommandArgument>
|
||||
): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(username: RedisArgument, command: Array<RedisArgument>) {
|
||||
return [
|
||||
'ACL',
|
||||
'DRYRUN',
|
||||
username,
|
||||
...command
|
||||
'ACL',
|
||||
'DRYRUN',
|
||||
username,
|
||||
...command
|
||||
];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply
|
||||
} as const satisfies Command;
|
||||
|
||||
|
@@ -1,23 +1,30 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_GENPASS';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import ACL_GENPASS from './ACL_GENPASS';
|
||||
|
||||
describe('ACL GENPASS', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'GENPASS']
|
||||
);
|
||||
});
|
||||
|
||||
it('with bits', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(128),
|
||||
['ACL', 'GENPASS', '128']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
ACL_GENPASS.transformArguments(),
|
||||
['ACL', 'GENPASS']
|
||||
);
|
||||
});
|
||||
|
||||
it('with bits', () => {
|
||||
assert.deepEqual(
|
||||
ACL_GENPASS.transformArguments(128),
|
||||
['ACL', 'GENPASS', '128']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclGenPass', async client => {
|
||||
assert.equal(
|
||||
typeof await client.aclGenPass(),
|
||||
'string'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(bits?: number): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(bits?: number) {
|
||||
const args = ['ACL', 'GENPASS'];
|
||||
|
||||
if (bits) {
|
||||
args.push(bits.toString());
|
||||
args.push(bits.toString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
},
|
||||
transformReply: undefined as unknown as () => BlobStringReply
|
||||
} as const satisfies Command;
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
|
@@ -1,34 +1,34 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_GETUSER';
|
||||
import ACL_GETUSER from './ACL_GETUSER';
|
||||
|
||||
describe('ACL GETUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username'),
|
||||
['ACL', 'GETUSER', 'username']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_GETUSER.transformArguments('username'),
|
||||
['ACL', 'GETUSER', 'username']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclGetUser', async client => {
|
||||
const reply = await client.aclGetUser('default');
|
||||
testUtils.testWithClient('client.aclGetUser', async client => {
|
||||
const reply = await client.aclGetUser('default');
|
||||
|
||||
assert.ok(Array.isArray(reply.passwords));
|
||||
assert.equal(typeof reply.commands, 'string');
|
||||
assert.ok(Array.isArray(reply.flags));
|
||||
assert.ok(Array.isArray(reply.passwords));
|
||||
assert.equal(typeof reply.commands, 'string');
|
||||
assert.ok(Array.isArray(reply.flags));
|
||||
|
||||
if (testUtils.isVersionGreaterThan([7])) {
|
||||
assert.equal(typeof reply.keys, 'string');
|
||||
assert.equal(typeof reply.channels, 'string');
|
||||
assert.ok(Array.isArray(reply.selectors));
|
||||
} else {
|
||||
assert.ok(Array.isArray(reply.keys));
|
||||
if (testUtils.isVersionGreaterThan([7])) {
|
||||
assert.equal(typeof reply.keys, 'string');
|
||||
assert.equal(typeof reply.channels, 'string');
|
||||
assert.ok(Array.isArray(reply.selectors));
|
||||
} else {
|
||||
assert.ok(Array.isArray(reply.keys));
|
||||
|
||||
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||
assert.ok(Array.isArray(reply.channels));
|
||||
}
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||
assert.ok(Array.isArray(reply.channels));
|
||||
}
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,40 +1,43 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(username: RedisCommandArgument): RedisCommandArguments {
|
||||
type AclUser = TuplesToMapReply<[
|
||||
[BlobStringReply<'flags'>, ArrayReply<BlobStringReply>],
|
||||
[BlobStringReply<'passwords'>, ArrayReply<BlobStringReply>],
|
||||
[BlobStringReply<'commands'>, BlobStringReply],
|
||||
/** changed to BlobStringReply in 7.0 */
|
||||
[BlobStringReply<'keys'>, ArrayReply<BlobStringReply> | BlobStringReply],
|
||||
/** added in 6.2, changed to BlobStringReply in 7.0 */
|
||||
[BlobStringReply<'channels'>, ArrayReply<BlobStringReply> | BlobStringReply],
|
||||
/** added in 7.0 */
|
||||
[BlobStringReply<'selectors'>, ArrayReply<TuplesToMapReply<[
|
||||
[BlobStringReply<'commands'>, BlobStringReply],
|
||||
[BlobStringReply<'keys'>, BlobStringReply],
|
||||
[BlobStringReply<'channels'>, BlobStringReply]
|
||||
]>>],
|
||||
]>;
|
||||
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(username: RedisArgument) {
|
||||
return ['ACL', 'GETUSER', username];
|
||||
}
|
||||
|
||||
type AclGetUserRawReply = [
|
||||
'flags',
|
||||
Array<RedisCommandArgument>,
|
||||
'passwords',
|
||||
Array<RedisCommandArgument>,
|
||||
'commands',
|
||||
RedisCommandArgument,
|
||||
'keys',
|
||||
Array<RedisCommandArgument> | RedisCommandArgument,
|
||||
'channels',
|
||||
Array<RedisCommandArgument> | RedisCommandArgument,
|
||||
'selectors' | undefined,
|
||||
Array<Array<string>> | undefined
|
||||
];
|
||||
|
||||
interface AclUser {
|
||||
flags: Array<RedisCommandArgument>;
|
||||
passwords: Array<RedisCommandArgument>;
|
||||
commands: RedisCommandArgument;
|
||||
keys: Array<RedisCommandArgument> | RedisCommandArgument;
|
||||
channels: Array<RedisCommandArgument> | RedisCommandArgument;
|
||||
selectors?: Array<Array<string>>;
|
||||
}
|
||||
|
||||
export function transformReply(reply: AclGetUserRawReply): AclUser {
|
||||
return {
|
||||
flags: reply[1],
|
||||
passwords: reply[3],
|
||||
commands: reply[5],
|
||||
keys: reply[7],
|
||||
channels: reply[9],
|
||||
selectors: reply[11]
|
||||
};
|
||||
}
|
||||
},
|
||||
transformReply: {
|
||||
2: (reply: UnwrapReply<Resp2Reply<AclUser>>) => ({
|
||||
flags: reply[1],
|
||||
passwords: reply[3],
|
||||
commands: reply[5],
|
||||
keys: reply[7],
|
||||
channels: reply[9],
|
||||
selectors: (reply[11] as unknown as UnwrapReply<typeof reply[11]>)?.map(selector => {
|
||||
const inferred = selector as unknown as UnwrapReply<typeof selector>;
|
||||
return {
|
||||
commands: inferred[1],
|
||||
keys: inferred[3],
|
||||
channels: inferred[5]
|
||||
};
|
||||
})
|
||||
}),
|
||||
3: undefined as unknown as () => AclUser
|
||||
}
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,22 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LIST';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import ACL_LIST from './ACL_LIST';
|
||||
|
||||
describe('ACL LIST', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LIST']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_LIST.transformArguments(),
|
||||
['ACL', 'LIST']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclList', async client => {
|
||||
const users = await client.aclList();
|
||||
assert.ok(Array.isArray(users));
|
||||
for (const user of users) {
|
||||
assert.equal(typeof user, 'string');
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ACL', 'LIST'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<RedisCommandArgument>;
|
||||
},
|
||||
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SAVE';
|
||||
import ACL_LOAD from './ACL_LOAD';
|
||||
|
||||
describe('ACL SAVE', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
describe('ACL LOAD', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'SAVE']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_LOAD.transformArguments(),
|
||||
['ACL', 'LOAD']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ACL', 'LOAD'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,53 +1,50 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments, transformReply } from './ACL_LOG';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import ACL_LOG from './ACL_LOG';
|
||||
|
||||
describe('ACL LOG', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOG']
|
||||
);
|
||||
});
|
||||
|
||||
it('with count', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(10),
|
||||
['ACL', 'LOG', '10']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
ACL_LOG.transformArguments(),
|
||||
['ACL', 'LOG']
|
||||
);
|
||||
});
|
||||
|
||||
it('transformReply', () => {
|
||||
assert.deepEqual(
|
||||
transformReply([[
|
||||
'count',
|
||||
1,
|
||||
'reason',
|
||||
'auth',
|
||||
'context',
|
||||
'toplevel',
|
||||
'object',
|
||||
'AUTH',
|
||||
'username',
|
||||
'someuser',
|
||||
'age-seconds',
|
||||
'4.096',
|
||||
'client-info',
|
||||
'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
|
||||
]]),
|
||||
[{
|
||||
count: 1,
|
||||
reason: 'auth',
|
||||
context: 'toplevel',
|
||||
object: 'AUTH',
|
||||
username: 'someuser',
|
||||
ageSeconds: 4.096,
|
||||
clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
|
||||
}]
|
||||
);
|
||||
it('with count', () => {
|
||||
assert.deepEqual(
|
||||
ACL_LOG.transformArguments(10),
|
||||
['ACL', 'LOG', '10']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclLog', async client => {
|
||||
// make sure to create one log
|
||||
await assert.rejects(
|
||||
client.auth({
|
||||
username: 'incorrect',
|
||||
password: 'incorrect'
|
||||
})
|
||||
);
|
||||
|
||||
const logs = await client.aclLog();
|
||||
assert.ok(Array.isArray(logs));
|
||||
for (const log of logs) {
|
||||
assert.equal(typeof log.count, 'number');
|
||||
assert.equal(typeof log.reason, 'string');
|
||||
assert.equal(typeof log.context, 'string');
|
||||
assert.equal(typeof log.object, 'string');
|
||||
assert.equal(typeof log.username, 'string');
|
||||
assert.equal(typeof log['age-seconds'], 'number');
|
||||
assert.equal(typeof log['client-info'], 'string');
|
||||
if (testUtils.isVersionGreaterThan([7, 2])) {
|
||||
assert.equal(typeof log['entry-id'], 'number');
|
||||
assert.equal(typeof log['timestamp-created'], 'number');
|
||||
assert.equal(typeof log['timestamp-last-updated'], 'number');
|
||||
}
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,50 +1,52 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types';
|
||||
import { transformDoubleReply } from './generic-transformers';
|
||||
|
||||
export function transformArguments(count?: number): RedisCommandArguments {
|
||||
export type AclLogReply = ArrayReply<TuplesToMapReply<[
|
||||
[BlobStringReply<'count'>, NumberReply],
|
||||
[BlobStringReply<'reason'>, BlobStringReply],
|
||||
[BlobStringReply<'context'>, BlobStringReply],
|
||||
[BlobStringReply<'object'>, BlobStringReply],
|
||||
[BlobStringReply<'username'>, BlobStringReply],
|
||||
[BlobStringReply<'age-seconds'>, DoubleReply],
|
||||
[BlobStringReply<'client-info'>, BlobStringReply],
|
||||
/** added in 7.0 */
|
||||
[BlobStringReply<'entry-id'>, NumberReply],
|
||||
/** added in 7.0 */
|
||||
[BlobStringReply<'timestamp-created'>, NumberReply],
|
||||
/** added in 7.0 */
|
||||
[BlobStringReply<'timestamp-last-updated'>, NumberReply]
|
||||
]>>;
|
||||
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(count?: number) {
|
||||
const args = ['ACL', 'LOG'];
|
||||
|
||||
if (count) {
|
||||
args.push(count.toString());
|
||||
if (count !== undefined) {
|
||||
args.push(count.toString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
type AclLogRawReply = [
|
||||
_: RedisCommandArgument,
|
||||
count: number,
|
||||
_: RedisCommandArgument,
|
||||
reason: RedisCommandArgument,
|
||||
_: RedisCommandArgument,
|
||||
context: RedisCommandArgument,
|
||||
_: RedisCommandArgument,
|
||||
object: RedisCommandArgument,
|
||||
_: RedisCommandArgument,
|
||||
username: RedisCommandArgument,
|
||||
_: RedisCommandArgument,
|
||||
ageSeconds: RedisCommandArgument,
|
||||
_: RedisCommandArgument,
|
||||
clientInfo: RedisCommandArgument
|
||||
];
|
||||
|
||||
interface AclLog {
|
||||
count: number;
|
||||
reason: RedisCommandArgument;
|
||||
context: RedisCommandArgument;
|
||||
object: RedisCommandArgument;
|
||||
username: RedisCommandArgument;
|
||||
ageSeconds: number;
|
||||
clientInfo: RedisCommandArgument;
|
||||
}
|
||||
|
||||
export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
|
||||
return reply.map(log => ({
|
||||
count: log[1],
|
||||
reason: log[3],
|
||||
context: log[5],
|
||||
object: log[7],
|
||||
username: log[9],
|
||||
ageSeconds: Number(log[11]),
|
||||
clientInfo: log[13]
|
||||
}));
|
||||
}
|
||||
},
|
||||
transformReply: {
|
||||
2: (reply: UnwrapReply<Resp2Reply<AclLogReply>>, preserve?: any, typeMapping?: TypeMapping) => {
|
||||
return reply.map(item => {
|
||||
const inferred = item as unknown as UnwrapReply<typeof item>;
|
||||
return {
|
||||
count: inferred[1],
|
||||
reason: inferred[3],
|
||||
context: inferred[5],
|
||||
object: inferred[7],
|
||||
username: inferred[9],
|
||||
'age-seconds': transformDoubleReply[2](inferred[11], preserve, typeMapping),
|
||||
'client-info': inferred[13],
|
||||
'entry-id': inferred[15],
|
||||
'timestamp-created': inferred[17],
|
||||
'timestamp-last-updated': inferred[19]
|
||||
};
|
||||
})
|
||||
},
|
||||
3: undefined as unknown as () => AclLogReply
|
||||
}
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,21 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOG_RESET';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import ACL_LOG_RESET from './ACL_LOG_RESET';
|
||||
|
||||
describe('ACL LOG RESET', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOG', 'RESET']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_LOG_RESET.transformArguments(),
|
||||
['ACL', 'LOG', 'RESET']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclLogReset', async client => {
|
||||
assert.equal(
|
||||
await client.aclLogReset(),
|
||||
'OK'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
import ACL_LOG from './ACL_LOG';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: ACL_LOG.IS_READ_ONLY,
|
||||
transformArguments() {
|
||||
return ['ACL', 'LOG', 'RESET'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOAD';
|
||||
import ACL_SAVE from './ACL_SAVE';
|
||||
|
||||
describe('ACL LOAD', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
describe('ACL SAVE', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOAD']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_SAVE.transformArguments(),
|
||||
['ACL', 'SAVE']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ACL', 'SAVE'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SETUSER';
|
||||
import ACL_SETUSER from './ACL_SETUSER';
|
||||
|
||||
describe('ACL SETUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username', 'allkeys'),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys']
|
||||
);
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username', ['allkeys', 'allchannels']),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
ACL_SETUSER.transformArguments('username', 'allkeys'),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys']
|
||||
);
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
ACL_SETUSER.transformArguments('username', ['allkeys', 'allchannels']),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
|
||||
|
||||
export function transformArguments(
|
||||
username: RedisCommandArgument,
|
||||
rule: RedisCommandArgument | Array<RedisCommandArgument>
|
||||
): RedisCommandArguments {
|
||||
return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(username: RedisArgument, rule: RedisVariadicArgument) {
|
||||
return pushVariadicArguments(['ACL', 'SETUSER', username], rule);
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_USERS';
|
||||
import ACL_USERS from './ACL_USERS';
|
||||
|
||||
describe('ACL USERS', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'USERS']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_USERS.transformArguments(),
|
||||
['ACL', 'USERS']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ACL', 'USERS'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<RedisCommandArgument>;
|
||||
},
|
||||
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_WHOAMI';
|
||||
import ACL_WHOAMI from './ACL_WHOAMI';
|
||||
|
||||
describe('ACL WHOAMI', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'WHOAMI']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ACL_WHOAMI.transformArguments(),
|
||||
['ACL', 'WHOAMI']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { BlobStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ACL', 'WHOAMI'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => BlobStringReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,11 +1,22 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './APPEND';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import APPEND from './APPEND';
|
||||
|
||||
describe('APPEND', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 'value'),
|
||||
['APPEND', 'key', 'value']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
APPEND.transformArguments('key', 'value'),
|
||||
['APPEND', 'key', 'value']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testAll('append', async client => {
|
||||
assert.equal(
|
||||
await client.append('key', 'value'),
|
||||
5
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, NumberReply, Command } from '../RESP/types';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
key: RedisCommandArgument,
|
||||
value: RedisCommandArgument
|
||||
): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(key: RedisArgument, value: RedisArgument) {
|
||||
return ['APPEND', key, value];
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
||||
},
|
||||
transformReply: undefined as unknown as () => NumberReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './ASKING';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import ASKING from './ASKING';
|
||||
|
||||
describe('ASKING', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ASKING']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
ASKING.transformArguments(),
|
||||
['ASKING']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArguments, RedisCommandArgument } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['ASKING'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,25 +1,25 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './AUTH';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import AUTH from './AUTH';
|
||||
|
||||
describe('AUTH', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('password only', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'password']
|
||||
);
|
||||
});
|
||||
|
||||
it('username & password', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'username', 'password']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('password only', () => {
|
||||
assert.deepEqual(
|
||||
AUTH.transformArguments({
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'password']
|
||||
);
|
||||
});
|
||||
|
||||
it('username & password', () => {
|
||||
assert.deepEqual(
|
||||
AUTH.transformArguments({
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'username', 'password']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,16 +1,23 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
export interface AuthOptions {
|
||||
username?: RedisCommandArgument;
|
||||
password: RedisCommandArgument;
|
||||
username?: RedisArgument;
|
||||
password: RedisArgument;
|
||||
}
|
||||
|
||||
export function transformArguments({ username, password }: AuthOptions): RedisCommandArguments {
|
||||
if (!username) {
|
||||
return ['AUTH', password];
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments({ username, password }: AuthOptions) {
|
||||
const args: Array<RedisArgument> = ['AUTH'];
|
||||
|
||||
if (username !== undefined) {
|
||||
args.push(username);
|
||||
}
|
||||
|
||||
return ['AUTH', username, password];
|
||||
}
|
||||
args.push(password);
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
return args;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,11 +1,19 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './BGREWRITEAOF';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import BGREWRITEAOF from './BGREWRITEAOF';
|
||||
|
||||
describe('BGREWRITEAOF', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGREWRITEAOF']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
BGREWRITEAOF.transformArguments(),
|
||||
['BGREWRITEAOF']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bgRewriteAof', async client => {
|
||||
assert.equal(
|
||||
typeof await client.bgRewriteAof(),
|
||||
'string'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
export function transformArguments(): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments() {
|
||||
return ['BGREWRITEAOF'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,23 +1,32 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describe } from 'mocha';
|
||||
import { transformArguments } from './BGSAVE';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import BGSAVE from './BGSAVE';
|
||||
|
||||
describe('BGSAVE', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGSAVE']
|
||||
);
|
||||
});
|
||||
|
||||
it('with SCHEDULE', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
SCHEDULE: true
|
||||
}),
|
||||
['BGSAVE', 'SCHEDULE']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BGSAVE.transformArguments(),
|
||||
['BGSAVE']
|
||||
);
|
||||
});
|
||||
|
||||
it('with SCHEDULE', () => {
|
||||
assert.deepEqual(
|
||||
BGSAVE.transformArguments({
|
||||
SCHEDULE: true
|
||||
}),
|
||||
['BGSAVE', 'SCHEDULE']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bgSave', async client => {
|
||||
assert.equal(
|
||||
typeof await client.bgSave({
|
||||
SCHEDULE: true // using `SCHEDULE` to make sure it won't throw an error
|
||||
}),
|
||||
'string'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SimpleStringReply, Command } from '../RESP/types';
|
||||
|
||||
interface BgSaveOptions {
|
||||
SCHEDULE?: true;
|
||||
export interface BgSaveOptions {
|
||||
SCHEDULE?: boolean;
|
||||
}
|
||||
|
||||
export function transformArguments(options?: BgSaveOptions): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: undefined,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(options?: BgSaveOptions) {
|
||||
const args = ['BGSAVE'];
|
||||
|
||||
|
||||
if (options?.SCHEDULE) {
|
||||
args.push('SCHEDULE');
|
||||
args.push('SCHEDULE');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument;
|
||||
},
|
||||
transformReply: undefined as unknown as () => SimpleStringReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,44 +1,47 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITCOUNT';
|
||||
import BITCOUNT from './BITCOUNT';
|
||||
|
||||
describe('BITCOUNT', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key'),
|
||||
['BITCOUNT', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
describe('with range', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', {
|
||||
start: 0,
|
||||
end: 1
|
||||
}),
|
||||
['BITCOUNT', 'key', '0', '1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with mode', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', {
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'BIT'
|
||||
}),
|
||||
['BITCOUNT', 'key', '0', '1', 'BIT']
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BITCOUNT.transformArguments('key'),
|
||||
['BITCOUNT', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitCount', async client => {
|
||||
assert.equal(
|
||||
await client.bitCount('key'),
|
||||
0
|
||||
describe('with range', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BITCOUNT.transformArguments('key', {
|
||||
start: 0,
|
||||
end: 1
|
||||
}),
|
||||
['BITCOUNT', 'key', '0', '1']
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
||||
it('with mode', () => {
|
||||
assert.deepEqual(
|
||||
BITCOUNT.transformArguments('key', {
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'BIT'
|
||||
}),
|
||||
['BITCOUNT', 'key', '0', '1', 'BIT']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testAll('bitCount', async client => {
|
||||
assert.equal(
|
||||
await client.bitCount('key'),
|
||||
0
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,33 +1,29 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, NumberReply, Command } from '../RESP/types';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
interface BitCountRange {
|
||||
start: number;
|
||||
end: number;
|
||||
mode?: 'BYTE' | 'BIT';
|
||||
export interface BitCountRange {
|
||||
start: number;
|
||||
end: number;
|
||||
mode?: 'BYTE' | 'BIT';
|
||||
}
|
||||
|
||||
export function transformArguments(
|
||||
key: RedisCommandArgument,
|
||||
range?: BitCountRange
|
||||
): RedisCommandArguments {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(key: RedisArgument, range?: BitCountRange) {
|
||||
const args = ['BITCOUNT', key];
|
||||
|
||||
if (range) {
|
||||
args.push(
|
||||
range.start.toString(),
|
||||
range.end.toString()
|
||||
);
|
||||
args.push(
|
||||
range.start.toString(),
|
||||
range.end.toString()
|
||||
);
|
||||
|
||||
if (range.mode) {
|
||||
args.push(range.mode);
|
||||
}
|
||||
if (range.mode) {
|
||||
args.push(range.mode);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
||||
},
|
||||
transformReply: undefined as unknown as () => NumberReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,46 +1,55 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITFIELD';
|
||||
import BITFIELD from './BITFIELD';
|
||||
|
||||
describe('BITFIELD', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', [{
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'WRAP'
|
||||
}, {
|
||||
operation: 'GET',
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'SAT'
|
||||
}, {
|
||||
operation: 'SET',
|
||||
encoding: 'i16',
|
||||
offset: 1,
|
||||
value: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'FAIL'
|
||||
}, {
|
||||
operation: 'INCRBY',
|
||||
encoding: 'i32',
|
||||
offset: 2,
|
||||
increment: 1
|
||||
}]),
|
||||
['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
BITFIELD.transformArguments('key', [{
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'WRAP'
|
||||
}, {
|
||||
operation: 'GET',
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'SAT'
|
||||
}, {
|
||||
operation: 'SET',
|
||||
encoding: 'i16',
|
||||
offset: 1,
|
||||
value: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'FAIL'
|
||||
}, {
|
||||
operation: 'INCRBY',
|
||||
encoding: 'i32',
|
||||
offset: 2,
|
||||
increment: 1
|
||||
}]),
|
||||
['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitField', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bitField('key', [{
|
||||
operation: 'GET',
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
[0]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('bitField', async client => {
|
||||
const a = client.bitField('key', [{
|
||||
operation: 'GET',
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]);
|
||||
|
||||
assert.deepEqual(
|
||||
await client.bitField('key', [{
|
||||
operation: 'GET',
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
[0]
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,80 +1,87 @@
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '../RESP/types';
|
||||
|
||||
export type BitFieldEncoding = `${'i' | 'u'}${number}`;
|
||||
|
||||
export interface BitFieldOperation<S extends string> {
|
||||
operation: S;
|
||||
operation: S;
|
||||
}
|
||||
|
||||
export interface BitFieldGetOperation extends BitFieldOperation<'GET'> {
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
}
|
||||
|
||||
interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
value: number;
|
||||
export interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
increment: number;
|
||||
export interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
|
||||
encoding: BitFieldEncoding;
|
||||
offset: number | string;
|
||||
increment: number;
|
||||
}
|
||||
|
||||
interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
|
||||
behavior: string;
|
||||
export interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
|
||||
behavior: string;
|
||||
}
|
||||
|
||||
type BitFieldOperations = Array<
|
||||
BitFieldGetOperation |
|
||||
BitFieldSetOperation |
|
||||
BitFieldIncrByOperation |
|
||||
BitFieldOverflowOperation
|
||||
export type BitFieldOperations = Array<
|
||||
BitFieldGetOperation |
|
||||
BitFieldSetOperation |
|
||||
BitFieldIncrByOperation |
|
||||
BitFieldOverflowOperation
|
||||
>;
|
||||
|
||||
export function transformArguments(key: string, operations: BitFieldOperations): Array<string> {
|
||||
export type BitFieldRoOperations = Array<
|
||||
Omit<BitFieldGetOperation, 'operation'>
|
||||
>;
|
||||
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(key: RedisArgument, operations: BitFieldOperations) {
|
||||
const args = ['BITFIELD', key];
|
||||
|
||||
for (const options of operations) {
|
||||
switch (options.operation) {
|
||||
case 'GET':
|
||||
args.push(
|
||||
'GET',
|
||||
options.encoding,
|
||||
options.offset.toString()
|
||||
);
|
||||
break;
|
||||
switch (options.operation) {
|
||||
case 'GET':
|
||||
args.push(
|
||||
'GET',
|
||||
options.encoding,
|
||||
options.offset.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'SET':
|
||||
args.push(
|
||||
'SET',
|
||||
options.encoding,
|
||||
options.offset.toString(),
|
||||
options.value.toString()
|
||||
);
|
||||
break;
|
||||
case 'SET':
|
||||
args.push(
|
||||
'SET',
|
||||
options.encoding,
|
||||
options.offset.toString(),
|
||||
options.value.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'INCRBY':
|
||||
args.push(
|
||||
'INCRBY',
|
||||
options.encoding,
|
||||
options.offset.toString(),
|
||||
options.increment.toString()
|
||||
);
|
||||
break;
|
||||
case 'INCRBY':
|
||||
args.push(
|
||||
'INCRBY',
|
||||
options.encoding,
|
||||
options.offset.toString(),
|
||||
options.increment.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'OVERFLOW':
|
||||
args.push(
|
||||
'OVERFLOW',
|
||||
options.behavior
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'OVERFLOW':
|
||||
args.push(
|
||||
'OVERFLOW',
|
||||
options.behavior
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<number | null>;
|
||||
},
|
||||
transformReply: undefined as unknown as () => ArrayReply<NumberReply | NullReply>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,27 +1,30 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITFIELD_RO';
|
||||
import BITFIELD_RO from './BITFIELD_RO';
|
||||
|
||||
describe('BITFIELD RO', () => {
|
||||
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||
describe('BITFIELD_RO', () => {
|
||||
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', [{
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
['BITFIELD_RO', 'key', 'GET', 'i8', '0']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
BITFIELD_RO.transformArguments('key', [{
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
['BITFIELD_RO', 'key', 'GET', 'i8', '0']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitFieldRo', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bitFieldRo('key', [{
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
[0]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('bitFieldRo', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bitFieldRo('key', [{
|
||||
encoding: 'i8',
|
||||
offset: 0
|
||||
}]),
|
||||
[0]
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,26 +1,25 @@
|
||||
import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
|
||||
import { BitFieldGetOperation } from './BITFIELD';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
type BitFieldRoOperations = Array<
|
||||
Omit<BitFieldGetOperation, 'operation'> &
|
||||
Partial<Pick<BitFieldGetOperation, 'operation'>>
|
||||
export type BitFieldRoOperations = Array<
|
||||
Omit<BitFieldGetOperation, 'operation'>
|
||||
>;
|
||||
|
||||
export function transformArguments(key: string, operations: BitFieldRoOperations): Array<string> {
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(key: RedisArgument, operations: BitFieldRoOperations) {
|
||||
const args = ['BITFIELD_RO', key];
|
||||
|
||||
for (const operation of operations) {
|
||||
args.push(
|
||||
'GET',
|
||||
operation.encoding,
|
||||
operation.offset.toString()
|
||||
);
|
||||
args.push(
|
||||
'GET',
|
||||
operation.encoding,
|
||||
operation.offset.toString()
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<number | null>;
|
||||
},
|
||||
transformReply: undefined as unknown as () => ArrayReply<NumberReply>
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,35 +1,31 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITOP';
|
||||
import BITOP from './BITOP';
|
||||
|
||||
describe('BITOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single key', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('AND', 'destKey', 'key'),
|
||||
['BITOP', 'AND', 'destKey', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple keys', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('AND', 'destKey', ['1', '2']),
|
||||
['BITOP', 'AND', 'destKey', '1', '2']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('single key', () => {
|
||||
assert.deepEqual(
|
||||
BITOP.transformArguments('AND', 'destKey', 'key'),
|
||||
['BITOP', 'AND', 'destKey', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitOp', async client => {
|
||||
assert.equal(
|
||||
await client.bitOp('AND', 'destKey', 'key'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('multiple keys', () => {
|
||||
assert.deepEqual(
|
||||
BITOP.transformArguments('AND', 'destKey', ['1', '2']),
|
||||
['BITOP', 'AND', 'destKey', '1', '2']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('cluster.bitOp', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testAll('bitOp', async client => {
|
||||
assert.equal(
|
||||
await client.bitOp('AND', '{tag}destKey', '{tag}key'),
|
||||
0
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
import { NumberReply, Command, RedisArgument } from '../RESP/types';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 2;
|
||||
export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
|
||||
|
||||
type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
|
||||
|
||||
export function transformArguments(
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 2,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(
|
||||
operation: BitOperations,
|
||||
destKey: RedisCommandArgument,
|
||||
key: RedisCommandArgument | Array<RedisCommandArgument>
|
||||
): RedisCommandArguments {
|
||||
return pushVerdictArguments(['BITOP', operation, destKey], key);
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
||||
destKey: RedisArgument,
|
||||
key: RedisVariadicArgument
|
||||
) {
|
||||
return pushVariadicArguments(['BITOP', operation, destKey], key);
|
||||
},
|
||||
transformReply: undefined as unknown as () => NumberReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,49 +1,45 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITPOS';
|
||||
import BITPOS from './BITPOS';
|
||||
|
||||
describe('BITPOS', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1),
|
||||
['BITPOS', 'key', '1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1, 1),
|
||||
['BITPOS', 'key', '1', '1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start and end', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1, 1, -1),
|
||||
['BITPOS', 'key', '1', '1', '-1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start, end and mode', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1, 1, -1, 'BIT'),
|
||||
['BITPOS', 'key', '1', '1', '-1', 'BIT']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BITPOS.transformArguments('key', 1),
|
||||
['BITPOS', 'key', '1']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitPos', async client => {
|
||||
assert.equal(
|
||||
await client.bitPos('key', 1, 1),
|
||||
-1
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('with start', () => {
|
||||
assert.deepEqual(
|
||||
BITPOS.transformArguments('key', 1, 1),
|
||||
['BITPOS', 'key', '1', '1']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithCluster('cluster.bitPos', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.bitPos('key', 1, 1),
|
||||
-1
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
it('with start and end', () => {
|
||||
assert.deepEqual(
|
||||
BITPOS.transformArguments('key', 1, 1, -1),
|
||||
['BITPOS', 'key', '1', '1', '-1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start, end and mode', () => {
|
||||
assert.deepEqual(
|
||||
BITPOS.transformArguments('key', 1, 1, -1, 'BIT'),
|
||||
['BITPOS', 'key', '1', '1', '-1', 'BIT']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testAll('bitPos', async client => {
|
||||
assert.equal(
|
||||
await client.bitPos('key', 1, 1),
|
||||
-1
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,32 +1,31 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, NumberReply, Command } from '../RESP/types';
|
||||
import { BitValue } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(
|
||||
key: RedisCommandArgument,
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(
|
||||
key: RedisArgument,
|
||||
bit: BitValue,
|
||||
start?: number,
|
||||
end?: number,
|
||||
mode?: 'BYTE' | 'BIT'
|
||||
): RedisCommandArguments {
|
||||
) {
|
||||
const args = ['BITPOS', key, bit.toString()];
|
||||
|
||||
if (typeof start === 'number') {
|
||||
args.push(start.toString());
|
||||
if (start !== undefined) {
|
||||
args.push(start.toString());
|
||||
}
|
||||
|
||||
if (typeof end === 'number') {
|
||||
args.push(end.toString());
|
||||
if (end !== undefined) {
|
||||
args.push(end.toString());
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
args.push(mode);
|
||||
args.push(mode);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
||||
},
|
||||
transformReply: undefined as unknown as () => NumberReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,43 +1,35 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BLMOVE';
|
||||
import { commandOptions } from '../../index';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BLMOVE from './BLMOVE';
|
||||
|
||||
describe('BLMOVE', () => {
|
||||
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
|
||||
['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
BLMOVE.transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
|
||||
['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.blMove', async client => {
|
||||
const [blMoveReply] = await Promise.all([
|
||||
client.blMove(commandOptions({
|
||||
isolated: true
|
||||
}), 'source', 'destination', 'LEFT', 'RIGHT', 0),
|
||||
client.lPush('source', 'element')
|
||||
]);
|
||||
testUtils.testAll('blMove - null', async client => {
|
||||
assert.equal(
|
||||
await client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
blMoveReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.blMove', async cluster => {
|
||||
const [blMoveReply] = await Promise.all([
|
||||
cluster.blMove(commandOptions({
|
||||
isolated: true
|
||||
}), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0),
|
||||
cluster.lPush('{tag}source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
blMoveReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
testUtils.testAll('blMove - with member', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.lPush('{tag}source', 'element'),
|
||||
client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE)
|
||||
]);
|
||||
assert.equal(reply, 'element');
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,23 +1,24 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
|
||||
import { ListSide } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
source: RedisCommandArgument,
|
||||
destination: RedisCommandArgument,
|
||||
sourceDirection: ListSide,
|
||||
destinationDirection: ListSide,
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(
|
||||
source: RedisArgument,
|
||||
destination: RedisArgument,
|
||||
sourceSide: ListSide,
|
||||
destinationSide: ListSide,
|
||||
timeout: number
|
||||
): RedisCommandArguments {
|
||||
) {
|
||||
return [
|
||||
'BLMOVE',
|
||||
source,
|
||||
destination,
|
||||
sourceDirection,
|
||||
destinationDirection,
|
||||
timeout.toString()
|
||||
'BLMOVE',
|
||||
source,
|
||||
destination,
|
||||
sourceSide,
|
||||
destinationSide,
|
||||
timeout.toString()
|
||||
];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument | null;
|
||||
},
|
||||
transformReply: undefined as unknown as () => BlobStringReply | NullReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,32 +1,49 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BLMPOP';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BLMPOP from './BLMPOP';
|
||||
|
||||
describe('BLMPOP', () => {
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0, 'key', 'LEFT'),
|
||||
['BLMPOP', '0', '1', 'key', 'LEFT']
|
||||
);
|
||||
});
|
||||
|
||||
it('with COUNT', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0, 'key', 'LEFT', {
|
||||
COUNT: 2
|
||||
}),
|
||||
['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '2']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BLMPOP.transformArguments(0, 'key', 'LEFT'),
|
||||
['BLMPOP', '0', '1', 'key', 'LEFT']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.blmPop', async client => {
|
||||
assert.deepEqual(
|
||||
await client.blmPop(1, 'key', 'RIGHT'),
|
||||
null
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('with COUNT', () => {
|
||||
assert.deepEqual(
|
||||
BLMPOP.transformArguments(0, 'key', 'LEFT', {
|
||||
COUNT: 1
|
||||
}),
|
||||
['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '1']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testAll('blmPop - null', async client => {
|
||||
assert.equal(
|
||||
await client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT'),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
|
||||
testUtils.testAll('blmPop - with member', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.lPush('key', 'element'),
|
||||
client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT')
|
||||
]);
|
||||
assert.deepEqual(reply, [
|
||||
'key',
|
||||
['element']
|
||||
]);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,20 +1,17 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers';
|
||||
import { Command } from '../RESP/types';
|
||||
import LMPOP, { LMPopArguments, transformLMPopArguments } from './LMPOP';
|
||||
|
||||
export const FIRST_KEY_INDEX = 3;
|
||||
|
||||
export function transformArguments(
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 3,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(
|
||||
timeout: number,
|
||||
keys: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||
side: ListSide,
|
||||
options?: LMPopOptions
|
||||
): RedisCommandArguments {
|
||||
...args: LMPopArguments
|
||||
) {
|
||||
return transformLMPopArguments(
|
||||
['BLMPOP', timeout.toString()],
|
||||
keys,
|
||||
side,
|
||||
options
|
||||
['BLMPOP', timeout.toString()],
|
||||
...args
|
||||
);
|
||||
}
|
||||
|
||||
export { transformReply } from './LMPOP';
|
||||
},
|
||||
transformReply: LMPOP.transformReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,79 +1,46 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BLPOP';
|
||||
import { commandOptions } from '../../index';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BLPOP from './BLPOP';
|
||||
|
||||
describe('BLPOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BLPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['key1', 'key2'], 0),
|
||||
['BLPOP', 'key1', 'key2', '0']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
BLPOP.transformArguments('key', 0),
|
||||
['BLPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'element']),
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
});
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
BLPOP.transformArguments(['1', '2'], 0),
|
||||
['BLPOP', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.blPop', async client => {
|
||||
const [ blPopReply ] = await Promise.all([
|
||||
client.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
client.lPush('key', 'element'),
|
||||
]);
|
||||
testUtils.testAll('blPop - null', async client => {
|
||||
assert.equal(
|
||||
await client.blPop('key', BLOCKING_MIN_VALUE),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
blPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('blPop - with member', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.lPush('key', 'element'),
|
||||
client.blPop('key', 1)
|
||||
]);
|
||||
|
||||
testUtils.testWithCluster('cluster.blPop', async cluster => {
|
||||
const [ blPopReply ] = await Promise.all([
|
||||
cluster.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
cluster.lPush('key', 'element')
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
blPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
assert.deepEqual(reply, {
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
});
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,31 +1,23 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
keys: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(
|
||||
key: RedisVariadicArgument,
|
||||
timeout: number
|
||||
): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BLPOP'], keys);
|
||||
|
||||
) {
|
||||
const args = pushVariadicArguments(['BLPOP'], key);
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
type BLPopRawReply = null | [RedisCommandArgument, RedisCommandArgument];
|
||||
|
||||
type BLPopReply = null | {
|
||||
key: RedisCommandArgument;
|
||||
element: RedisCommandArgument;
|
||||
};
|
||||
|
||||
export function transformReply(reply: BLPopRawReply): BLPopReply {
|
||||
},
|
||||
transformReply(reply: UnwrapReply<NullReply | TuplesReply<[BlobStringReply, BlobStringReply]>>) {
|
||||
if (reply === null) return null;
|
||||
|
||||
return {
|
||||
key: reply[0],
|
||||
element: reply[1]
|
||||
key: reply[0],
|
||||
element: reply[1]
|
||||
};
|
||||
}
|
||||
}
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,79 +1,46 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BRPOP';
|
||||
import { commandOptions } from '../../index';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BRPOP from './BRPOP';
|
||||
|
||||
describe('BRPOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BRPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['key1', 'key2'], 0),
|
||||
['BRPOP', 'key1', 'key2', '0']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
BRPOP.transformArguments('key', 0),
|
||||
['BRPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'element']),
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
});
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
BRPOP.transformArguments(['1', '2'], 0),
|
||||
['BRPOP', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.brPop', async client => {
|
||||
const [ brPopReply ] = await Promise.all([
|
||||
client.brPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
client.lPush('key', 'element'),
|
||||
]);
|
||||
testUtils.testAll('brPop - null', async client => {
|
||||
assert.equal(
|
||||
await client.brPop('key', BLOCKING_MIN_VALUE),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
brPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('brPopblPop - with member', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.lPush('key', 'element'),
|
||||
client.brPop('key', 1)
|
||||
]);
|
||||
|
||||
testUtils.testWithCluster('cluster.brPop', async cluster => {
|
||||
const [ brPopReply ] = await Promise.all([
|
||||
cluster.brPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
cluster.lPush('key', 'element'),
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
brPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
assert.deepEqual(reply, {
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
});
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
import { Command } from '../RESP/types';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
|
||||
import BLPOP from './BLPOP';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
key: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: true,
|
||||
transformArguments(
|
||||
key: RedisVariadicArgument,
|
||||
timeout: number
|
||||
): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BRPOP'], key);
|
||||
|
||||
) {
|
||||
const args = pushVariadicArguments(['BRPOP'], key);
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export { transformReply } from './BLPOP';
|
||||
},
|
||||
transformReply: BLPOP.transformReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,47 +1,42 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BRPOPLPUSH';
|
||||
import { commandOptions } from '../../index';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BRPOPLPUSH from './BRPOPLPUSH';
|
||||
|
||||
describe('BRPOPLPUSH', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('source', 'destination', 0),
|
||||
['BRPOPLPUSH', 'source', 'destination', '0']
|
||||
);
|
||||
});
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
BRPOPLPUSH.transformArguments('source', 'destination', 0),
|
||||
['BRPOPLPUSH', 'source', 'destination', '0']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.brPopLPush', async client => {
|
||||
const [ popReply ] = await Promise.all([
|
||||
client.brPopLPush(
|
||||
commandOptions({ isolated: true }),
|
||||
'source',
|
||||
'destination',
|
||||
0
|
||||
),
|
||||
client.lPush('source', 'element')
|
||||
]);
|
||||
testUtils.testAll('brPopLPush - null', async client => {
|
||||
assert.equal(
|
||||
await client.brPopLPush(
|
||||
'{tag}source',
|
||||
'{tag}destination',
|
||||
BLOCKING_MIN_VALUE
|
||||
),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
popReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('brPopLPush - with member', async client => {
|
||||
const [, reply] = await Promise.all([
|
||||
client.lPush('{tag}source', 'element'),
|
||||
client.brPopLPush(
|
||||
'{tag}source',
|
||||
'{tag}destination',
|
||||
0
|
||||
)
|
||||
]);
|
||||
|
||||
testUtils.testWithCluster('cluster.brPopLPush', async cluster => {
|
||||
const [ popReply ] = await Promise.all([
|
||||
cluster.brPopLPush(
|
||||
commandOptions({ isolated: true }),
|
||||
'{tag}source',
|
||||
'{tag}destination',
|
||||
0
|
||||
),
|
||||
cluster.lPush('{tag}source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
popReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
assert.equal(reply, 'element');
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.CLUSTERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
source: RedisCommandArgument,
|
||||
destination: RedisCommandArgument,
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 1,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(
|
||||
source: RedisArgument,
|
||||
destination: RedisArgument,
|
||||
timeout: number
|
||||
): RedisCommandArguments {
|
||||
) {
|
||||
return ['BRPOPLPUSH', source, destination, timeout.toString()];
|
||||
}
|
||||
|
||||
export declare function transformReply(): RedisCommandArgument | null;
|
||||
},
|
||||
transformReply: undefined as unknown as () => BlobStringReply | NullReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,32 +1,55 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BZMPOP';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BZMPOP from './BZMPOP';
|
||||
|
||||
describe('BZMPOP', () => {
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
testUtils.isVersionGreaterThanHook([7]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0, 'key', 'MIN'),
|
||||
['BZMPOP', '0', '1', 'key', 'MIN']
|
||||
);
|
||||
});
|
||||
|
||||
it('with COUNT', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0, 'key', 'MIN', {
|
||||
COUNT: 2
|
||||
}),
|
||||
['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
BZMPOP.transformArguments(0, 'key', 'MIN'),
|
||||
['BZMPOP', '0', '1', 'key', 'MIN']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bzmPop', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bzmPop(1, 'key', 'MAX'),
|
||||
null
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
it('with COUNT', () => {
|
||||
assert.deepEqual(
|
||||
BZMPOP.transformArguments(0, 'key', 'MIN', {
|
||||
COUNT: 2
|
||||
}),
|
||||
['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testAll('bzmPop - null', async client => {
|
||||
assert.equal(
|
||||
await client.bzmPop(BLOCKING_MIN_VALUE, 'key', 'MAX'),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.SERVERS.OPEN
|
||||
});
|
||||
|
||||
testUtils.testAll('bzmPop - with member', async client => {
|
||||
const key = 'key',
|
||||
member = {
|
||||
value: 'a',
|
||||
score: 1
|
||||
},
|
||||
[, reply] = await Promise.all([
|
||||
client.zAdd(key, member),
|
||||
client.bzmPop(BLOCKING_MIN_VALUE, key, 'MAX')
|
||||
]);
|
||||
|
||||
assert.deepEqual(reply, {
|
||||
key,
|
||||
members: [member]
|
||||
});
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.SERVERS.OPEN
|
||||
});
|
||||
});
|
||||
|
@@ -1,20 +1,11 @@
|
||||
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||
import { SortedSetSide, transformZMPopArguments, ZMPopOptions } from './generic-transformers';
|
||||
import { Command } from '../RESP/types';
|
||||
import ZMPOP, { ZMPopArguments, transformZMPopArguments } from './ZMPOP';
|
||||
|
||||
export const FIRST_KEY_INDEX = 3;
|
||||
|
||||
export function transformArguments(
|
||||
timeout: number,
|
||||
keys: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||
side: SortedSetSide,
|
||||
options?: ZMPopOptions
|
||||
): RedisCommandArguments {
|
||||
return transformZMPopArguments(
|
||||
['BZMPOP', timeout.toString()],
|
||||
keys,
|
||||
side,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export { transformReply } from './ZMPOP';
|
||||
export default {
|
||||
FIRST_KEY_INDEX: 3,
|
||||
IS_READ_ONLY: false,
|
||||
transformArguments(timeout: number, ...args: ZMPopArguments) {
|
||||
return transformZMPopArguments(['BZMPOP', timeout.toString()], ...args);
|
||||
},
|
||||
transformReply: ZMPOP.transformReply
|
||||
} as const satisfies Command;
|
||||
|
@@ -1,65 +1,51 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BZPOPMAX';
|
||||
import { commandOptions } from '../../index';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils';
|
||||
import BZPOPMAX from './BZPOPMAX';
|
||||
|
||||
describe('BZPOPMAX', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BZPOPMAX', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['1', '2'], 0),
|
||||
['BZPOPMAX', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
BZPOPMAX.transformArguments('key', 0),
|
||||
['BZPOPMAX', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'value', '1']),
|
||||
{
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
});
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
BZPOPMAX.transformArguments(['1', '2'], 0),
|
||||
['BZPOPMAX', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bzPopMax', async client => {
|
||||
const [ bzPopMaxReply ] = await Promise.all([
|
||||
client.bzPopMax(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
client.zAdd('key', [{
|
||||
value: '1',
|
||||
score: 1
|
||||
}])
|
||||
]);
|
||||
testUtils.testAll('bzPopMax - null', async client => {
|
||||
assert.equal(
|
||||
await client.bzPopMax('key', BLOCKING_MIN_VALUE),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.SERVERS.OPEN
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
bzPopMaxReply,
|
||||
{
|
||||
key: 'key',
|
||||
value: '1',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
testUtils.testAll('bzPopMax - with member', async client => {
|
||||
const key = 'key',
|
||||
member = {
|
||||
value: 'a',
|
||||
score: 1
|
||||
},
|
||||
[, reply] = await Promise.all([
|
||||
client.zAdd(key, member),
|
||||
client.bzPopMax(key, BLOCKING_MIN_VALUE)
|
||||
]);
|
||||
|
||||
assert.deepEqual(reply, {
|
||||
key,
|
||||
...member
|
||||
});
|
||||
}, {
|
||||
client: GLOBAL.SERVERS.OPEN,
|
||||
cluster: GLOBAL.SERVERS.OPEN
|
||||
});
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user