From e95634b37582b5146c5c0d4782f1418f94138e29 Mon Sep 17 00:00:00 2001 From: Leibale Date: Thu, 22 Jun 2023 19:41:35 -0400 Subject: [PATCH] ref #2489 --- docs/todo.md | 9 +- packages/client/lib/RESP/decoder.spec.ts | 361 ++++++++++-------- .../lib/commands/ZRANK_WITHSCORE.spec.ts | 46 +++ .../client/lib/commands/ZRANK_WITHSCORE.ts | 30 ++ packages/client/lib/commands/index.ts | 6 + todo.md | 31 -- 6 files changed, 281 insertions(+), 202 deletions(-) create mode 100644 packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts create mode 100644 packages/client/lib/commands/ZRANK_WITHSCORE.ts delete mode 100644 todo.md diff --git a/docs/todo.md b/docs/todo.md index 4d9c91e2dd..157a7c8eaf 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,19 +1,16 @@ -# Leibale +# Client - Does `close`/`destory` actually close the connection from the Redis POV? Works with OSS, but what about Redis Enterprie? -# Docs - +Docs: - [v4 to v5](./v4-to-v5.md) - Legacy mode - [Command Options](./command-options.md) - [RESP](./RESP.md) -# Missing functionality +# Server - `HEXISTS`: accepts one field only, should be the same as `EXISTS` -# Replies - `String` -> `Double`: - `INCRBYFLOAT` - `HINCRBYFLOAT` diff --git a/packages/client/lib/RESP/decoder.spec.ts b/packages/client/lib/RESP/decoder.spec.ts index cff4fbe047..911baafb50 100644 --- a/packages/client/lib/RESP/decoder.spec.ts +++ b/packages/client/lib/RESP/decoder.spec.ts @@ -1,195 +1,226 @@ // import { strict as assert } from 'assert'; // import { SinonSpy, spy } from 'sinon'; -// import RESP2Decoder from './decoder'; -// import { ErrorReply } from '../../errors'; +// import { Decoder, RESP_TYPES } from './decoder'; +// import { ErrorReply } from '../errors'; +// import { TypeMapping } from './types'; -// interface DecoderAndSpies { -// decoder: RESP2Decoder; -// returnStringsAsBuffersSpy: SinonSpy; -// onReplySpy: SinonSpy; +// function createDecoderAndSpies(typeMapping: TypeMapping = {}) { +// const typeMappingSpy = spy(() => typeMapping), +// onReplySpy = spy(), +// onErrorReplySpy = spy(), +// onPushSpy = spy(); + +// return { +// decoder: new Decoder({ +// getTypeMapping: typeMappingSpy, +// onReply: onReplySpy, +// onErrorReply: onErrorReplySpy, +// onPush: onPushSpy +// }), +// typeMappingSpy, +// onReplySpy, +// onErrorReplySpy, +// onPushSpy +// }; // } -// 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)); -// } +// function writeChunks(stream: Decoder, buffer: Buffer) { +// let i = 0; +// while (i < buffer.length) { +// stream.write(buffer.subarray(i, ++i)); +// } // } // type Replies = Array>; -// interface TestsOptions { -// toWrite: Buffer; -// returnStringsAsBuffers: boolean; -// replies: Replies; +// function generateTests(toWrite: Buffer, tests: Array): void { +// for (const test of tests) { +// describe(test.name, () => { +// generateTests(toWrite, test); +// }); +// } // } -// 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); -// }); +// interface Test { +// typeMapping?: TypeMapping; +// replies?: Replies; +// errorReplies?: Replies; +// pushReplies?: Replies; // } -// function testReplies(spy: SinonSpy, replies: Replies): void { -// if (!replies) { -// assert.equal(spy.callCount, 0); -// return; -// } +// function genetareTypeTests(toWrite: Buffer, { typeMapping, replies, errorReplies, pushReplies }: Test) { +// const total = (replies?.length ?? 0) + (errorReplies?.length ?? 0) + (pushReplies?.length ?? 0); +// it('single chunk', () => { +// const { decoder, typeMappingSpy, onReplySpy, onErrorReplySpy, onPushSpy } = createDecoderAndSpies(typeMapping); +// decoder.write(toWrite); +// assert.equal(typeMappingSpy.callCount, total); +// testReplies(onReplySpy, replies); +// testReplies(onErrorReplySpy, errorReplies); +// testReplies(onPushSpy, pushReplies); +// }); -// assert.equal(spy.callCount, replies.length); -// for (const [i, reply] of replies.entries()) { -// assert.deepEqual( -// spy.getCall(i).args, -// reply -// ); -// } +// it('multiple chunks', () => { +// const { decoder, typeMappingSpy, onReplySpy, onErrorReplySpy, onPushSpy } = createDecoderAndSpies(typeMapping); +// writeChunks(decoder, toWrite); +// assert.equal(typeMappingSpy.callCount, total); +// testReplies(onReplySpy, replies); +// testReplies(onErrorReplySpy, errorReplies); +// testReplies(onPushSpy, pushReplies); +// }); +// } + +// 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('Null', () => { +// genetareTypeTests(Buffer.from('_\r\n'), { +// replies: [[null]] // }); +// }); -// describe('Error', () => { +// describe('Boolean', () => { +// genetareTypeTests(Buffer.from('#t\r\n'), { +// replies: [[null]] +// }); +// }); + +// describe('Number', () => { +// generateTests(Buffer.from(':-1\r\n')) +// describe('as number', () => { +// describe('-1', () => { // generateTests({ -// toWrite: Buffer.from('-ERR\r\n'), -// returnStringsAsBuffers: false, -// replies: [[new ErrorReply('ERR')]] +// toWrite: , +// replies: [[-1]] // }); +// }); + +// describe('0', () => { +// generateTests({ +// toWrite: Buffer.from(':0\r\n'), +// replies: [[0]] +// }); +// }); + +// describe('+1', () => { +// generateTests({ +// toWrite: Buffer.from(':+1\r\n'), +// replies: [[1]] +// }); +// }); +// }); +// }); + +// describe('Simple String', () => { +// describe('as strings', () => { +// generateTests({ +// toWrite: Buffer.from('+OK\r\n'), +// replies: [['OK']] +// }); // }); -// describe('Integer', () => { -// describe('-1', () => { -// generateTests({ -// toWrite: Buffer.from(':-1\r\n'), -// returnStringsAsBuffers: false, -// replies: [[-1]] -// }); -// }); +// describe('as buffers', () => { +// generateTests({ +// toWrite: Buffer.from('+OK\r\n'), +// typeMapping: { +// [RESP_TYPES.SIMPLE_STRING]: Buffer +// }, +// replies: [[Buffer.from('OK')]] +// }); +// }); +// }); -// describe('0', () => { -// generateTests({ -// toWrite: Buffer.from(':0\r\n'), -// returnStringsAsBuffers: false, -// replies: [[0]] -// }); -// }); +// describe('Error', () => { +// generateTests({ +// toWrite: Buffer.from('-ERR\r\n'), +// errorReplies: [[new ErrorReply('ERR')]] +// }); +// }); + + + +// describe('Bulk String', () => { +// describe('null', () => { +// generateTests({ +// toWrite: Buffer.from('$-1\r\n'), +// returnStringsAsBuffers: false, +// replies: [[null]] +// }); // }); -// 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('as strings', () => { +// generateTests({ +// toWrite: Buffer.from('$2\r\naa\r\n'), +// returnStringsAsBuffers: false, +// replies: [['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'), -// [] -// ]]] -// }); -// }); +// 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'), +// [] +// ]]] +// }); +// }); +// }); // }); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts new file mode 100644 index 0000000000..3d308d0d47 --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ZRANK_WITHSCORE from './ZRANK_WITHSCORE'; + +describe('ZRANK WITHSCORE', () => { + testUtils.isVersionGreaterThanHook([7, 2]); + + it('transformArguments', () => { + assert.deepEqual( + ZRANK_WITHSCORE.transformArguments('key', 'member'), + ['ZRANK', 'key', 'member', 'WITHSCORE'] + ); + }); + + testUtils.testAll('zRankWithScore - null', async client => { + assert.equal( + await client.zRankWithScore('key', 'member'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('zRankWithScore - with member', async client => { + const member = { + value: '1', + score: 1 + } + + const [, reply] = await Promise.all([ + client.zAdd('key', member), + client.zRankWithScore('key', member.value) + ]) + assert.deepEqual( + reply, + { + rank: 0, + score: 1 + } + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts new file mode 100644 index 0000000000..e68c31c20c --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -0,0 +1,30 @@ +import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, Command } from '../RESP/types'; +import ZRANK from './ZRANK'; + +export default { + FIRST_KEY_INDEX: ZRANK.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANK.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZRANK.transformArguments(...args); + redisArgs.push('WITHSCORE'); + return redisArgs; + }, + transformReply: { + 2: (reply: NullReply | TuplesReply<[NumberReply, BlobStringReply]>) => { + if (reply === null) return null; + + return { + rank: reply[0], + score: Number(reply[1]) + }; + }, + 3: (reply: NullReply | TuplesReply<[BlobStringReply, DoubleReply]>) => { + if (reply === null) return null; + + return { + rank: reply[0], + score: reply[1] + }; + } + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index ac013c94ed..6537d89913 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -286,6 +286,7 @@ import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES'; import ZRANGEBYSCORE from './ZRANGEBYSCORE'; import ZRANGESTORE from './ZRANGESTORE'; import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; +import ZRANK_WITHSCORE from './ZRANK_WITHSCORE'; import ZRANK from './ZRANK'; import ZREM from './ZREM'; import ZREMRANGEBYLEX from './ZREMRANGEBYLEX'; @@ -586,6 +587,7 @@ type ZRANGEBYSCORE_WITHSCORES = typeof import('./ZRANGEBYSCORE_WITHSCORES').defa type ZRANGEBYSCORE = typeof import('./ZRANGEBYSCORE').default; type ZRANGESTORE = typeof import('./ZRANGESTORE').default; type ZREMRANGEBYSCORE = typeof import('./ZREMRANGEBYSCORE').default; +type ZRANK_WITHSCORE = typeof import('./ZRANK_WITHSCORE').default; type ZRANK = typeof import('./ZRANK').default; type ZREM = typeof import('./ZREM').default; type ZREMRANGEBYLEX = typeof import('./ZREMRANGEBYLEX').default; @@ -1174,6 +1176,8 @@ type Commands = { zRangeByScore: ZRANGEBYSCORE; ZRANGESTORE: ZRANGESTORE; zRangeStore: ZRANGESTORE; + ZRANK_WITHSCORE: ZRANK_WITHSCORE; + zRankWithScore: ZRANK_WITHSCORE; ZRANK: ZRANK; zRank: ZRANK; ZREM: ZREM; @@ -1775,6 +1779,8 @@ export default { zRangeByScore: ZRANGEBYSCORE, ZRANGESTORE, zRangeStore: ZRANGESTORE, + ZRANK_WITHSCORE, + zRankWithScore: ZRANK_WITHSCORE, ZRANK, zRank: ZRANK, ZREM, diff --git a/todo.md b/todo.md deleted file mode 100644 index 3d7d0b3674..0000000000 --- a/todo.md +++ /dev/null @@ -1,31 +0,0 @@ -# return type \ missing documentation -- `XAUTOCLAIM` -- `XSETID` -- `ZUNION` - - -# create commands -- `ZREVRANGE` -- `ZREVRANGEBYLEX` -- `ZREVRANGEBYSCORE` - - -# waiting List categoreis -- Set -- Bitmap - -# fot leiba -- `BZMPOP.ts` -- `BZPOPMAX.ts` -- `BZPOPMIN.ts` -- `ZMPOP.ts` -- `ZPOPMAX.ts` -- `ZPOPMIN.ts` -- `ZREVRANGE WITHSCORE` -- `ZRANK WITHSCORE` -- `MIGRATE` -- `RESTORE` -- `WAITAOF` - -# other -