You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +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:
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user