1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00
This commit is contained in:
Leibale
2023-04-23 07:56:15 -04:00
parent 35d32e5b64
commit 9faa3e77c4
326 changed files with 14620 additions and 10931 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
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'),
[]
]]]
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import { strict as assert } from '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']
);
});
});

View 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;
}

View File

@@ -0,0 +1,417 @@
import { RedisScriptConfig, SHA1 } from '../lua-script';
import { TYPES } from './decoder';
import { VerbatimString } from './verbatim-string';
export type RespTypes = typeof TYPES;
export type RespTypesUnion = RespTypes[keyof RespTypes];
type RespType<
RESP_TYPE extends RespTypesUnion,
DEFAULT,
TYPES = never,
FLAG_TYPES = DEFAULT | TYPES
> = (DEFAULT | TYPES) & {
RESP_TYPE: RESP_TYPE;
DEFAULT: DEFAULT;
TYPES: TYPES;
FLAG: Flag<FLAG_TYPES>;
};
export type NullReply = RespType<
RespTypes['NULL'],
null
>;
export type BooleanReply<
T extends boolean = boolean
> = RespType<
RespTypes['BOOLEAN'],
T
>;
export type NumberReply<
T extends number = number
> = RespType<
RespTypes['NUMBER'],
T,
`${T}`,
number | string
>;
export type BigNumberReply<
T extends bigint = bigint
> = RespType<
RespTypes['BIG_NUMBER'],
T,
number | `${T}`,
bigint | number | string
>;
export type DoubleReply<
T extends number = number
> = RespType<
RespTypes['DOUBLE'],
T,
`${T}`,
number | string
>;
export type SimpleStringReply<
T extends string = string
> = RespType<
RespTypes['SIMPLE_STRING'],
T,
Buffer,
string | Buffer
>;
export type BlobStringReply<
T extends string = string
> = RespType<
RespTypes['BLOB_STRING'],
T,
Buffer,
string | Buffer
>;
export type VerbatimStringReply<
T extends string = string
> = RespType<
RespTypes['VERBATIM_STRING'],
T,
Buffer | VerbatimString,
string | Buffer | VerbatimString
>;
export type SimpleErrorReply = RespType<
RespTypes['SIMPLE_ERROR'],
Buffer
>;
export type BlobErrorReply = RespType<
RespTypes['BLOB_ERROR'],
Buffer
>;
export type ArrayReply<T> = RespType<
RespTypes['ARRAY'],
Array<T>,
never,
Array<any>
>;
export type TuplesReply<T extends [...Array<unknown>]> = RespType<
RespTypes['ARRAY'],
T,
never,
Array<any>
>;
export type SetReply<T> = RespType<
RespTypes['SET'],
Array<T>,
Set<T>,
Array<any> | Set<any>
>;
export type MapReply<K, V> = RespType<
RespTypes['MAP'],
{ [key: string]: V },
Map<K, V> | Array<K | V>,
Map<any, any> | Array<any>
>;
type MapKeyValue = [key: BlobStringReply, value: unknown];
type MapTuples = Array<MapKeyValue>;
export type TuplesToMapReply<T extends MapTuples> = RespType<
RespTypes['MAP'],
{
[P in T[number] as P[0] extends BlobStringReply<infer S> ? S : never]: P[1];
},
Map<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 |
// cannot reuse ArrayReply, SetReply and MapReply because of circular reference
RespType<
RespTypes['ARRAY'],
Array<ReplyUnion>
> |
RespType<
RespTypes['SET'],
Array<ReplyUnion>,
Set<ReplyUnion>
> |
RespType<
RespTypes['MAP'],
{ [key: string]: ReplyUnion },
Map<ReplyUnion, ReplyUnion> | Array<ReplyUnion | ReplyUnion>
>;
export type Reply = ReplyWithFlags<ReplyUnion, {}>;
export type Flag<T> = ((...args: any) => T) | (new (...args: any) => T);
type RespTypeUnion<T> = T extends RespType<RespTypesUnion, unknown, unknown, infer FLAG_TYPES> ? FLAG_TYPES : never;
export type Flags = {
[P in RespTypesUnion]?: Flag<RespTypeUnion<Extract<ReplyUnion, RespType<P, any, any, any>>>>;
};
type MapKey<
T,
FLAGS extends Flags
> = ReplyWithFlags<T, FLAGS & {
// simple and blob strings as map keys decoded as strings
[TYPES.SIMPLE_STRING]: StringConstructor;
[TYPES.BLOB_STRING]: StringConstructor;
}>;
export type ReplyWithFlags<
REPLY,
FLAGS extends Flags
> = (
// if REPLY is a type, extract the coresponding type from FLAGS or use the default type
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
FLAGS[RESP_TYPE] extends Flag<infer T> ?
ReplyWithFlags<Extract<DEFAULT | TYPES, T>, FLAGS> :
ReplyWithFlags<DEFAULT, FLAGS>
: (
// if REPLY is a known generic type, convert its generic arguments
// TODO: tuples?
REPLY extends Array<infer T> ? Array<ReplyWithFlags<T, FLAGS>> :
REPLY extends Set<infer T> ? Set<ReplyWithFlags<T, FLAGS>> :
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, FLAGS>, ReplyWithFlags<V, FLAGS>> :
// `Date` & `Buffer` are supersets of `Record`, so they need to be checked first
REPLY extends Date ? REPLY :
REPLY extends Buffer ? REPLY :
REPLY extends Record<PropertyKey, any> ? {
[P in keyof REPLY]: ReplyWithFlags<REPLY[P], FLAGS>;
} :
// otherwise, just return the REPLY as is
REPLY
)
);
export type TransformReply = (this: void, reply: any, preserve?: any) => 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;
POLICIES?: CommandPolicies;
transformArguments(this: void, ...args: Array<any>): CommandArguments;
TRANSFORM_LEGACY_REPLY?: boolean;
transformReply: TransformReply | Record<RespVersions, TransformReply>;
};
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;
}
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 RespTypes['DOUBLE'] ? BlobStringReply :
RESP_TYPE extends RespTypes['ARRAY'] | RespTypes['SET'] ? RespType<
RESP_TYPE,
Resp2Array<DEFAULT>
> :
RESP_TYPE extends RespTypes['MAP'] ? RespType<
RespTypes['ARRAY'],
Resp2Array<Extract<TYPES, Array<any>>>
> :
RespType<
RESP_TYPE,
DEFAULT,
TYPES
> :
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
Reply
);
export type CommandSignature<
COMMAND extends Command,
RESP extends RespVersions,
FLAGS extends Flags
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<ReplyWithFlags<CommandReply<COMMAND, RESP>, FLAGS>>;
export type CommandWithPoliciesSignature<
COMMAND extends Command,
RESP extends RespVersions,
FLAGS extends Flags,
POLICIES extends CommandPolicies
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<
ReplyWithPolicy<
ReplyWithFlags<CommandReply<COMMAND, RESP>, FLAGS>,
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>
);
const SAME = {
transformArguments(key: string): Array<string> {
return ['GET', key];
},
transformReply: () => 'default' as const
} satisfies Command;
type SAME_DEFAULT = CommandWithPoliciesSignature<
typeof SAME,
2,
{},
{
request: REQUEST_POLICIES['ALL_NODES'];
response: RESPONSE_POLICIES['SPECIAL'];
}
>;
// type SAME_RESP2 = CommandReply<typeof SAME, 2>;
// type SAME_COMMAND_RESP2 = CommandSignuture<typeof SAME, 2>;
// type SAME_RESP3 = CommandReply<typeof SAME, 3>;
// type SAME_COMMAND_RESP3 = CommandSignuture<typeof SAME, 3>;
// interface Test {
// /**
// * This is a test
// */
// a: 'a';
// }
// const DIFFERENT = {
// transformArguments(key: string): Array<string> {
// return ['GET', key];
// },
// transformReply: {
// 2: () => null as any as Test,
// 3: () => '3' as const
// }
// } satisfies Command;
// type DIFFERENT_RESP2 = CommandReply<typeof DIFFERENT, 2>;
// type DIFFERENT_COMMAND_RESP2 = CommandSignuture<typeof DIFFERENT, 2>;
// type DIFFERENT_RESP3 = CommandReply<typeof DIFFERENT, 3>;
// type DIFFERENT_COMMAND_RESP3 = CommandSignuture<typeof DIFFERENT, 3>;
// const a = null as any as DIFFERENT_COMMAND_RESP2;
// const b = await a('a');
// b.a

View File

@@ -0,0 +1,8 @@
export class VerbatimString extends String {
constructor(
public format: string,
value: string
) {
super(value);
}
}