diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 25c966c2719..ad4ba72a09e 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -2,13 +2,15 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisClient, { RedisClientType } from '.'; import { RedisClientMultiCommandType } from './multi-command'; -import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands'; +import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts, ConvertArgumentType } from '../commands'; import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; import { spy } from 'sinon'; import { once } from 'events'; import { ClientKillFilters } from '../commands/CLIENT_KILL'; import { promisify } from 'util'; +import { commandOptions } from '../../dist/lib/command-options'; +import { ZMember } from '../commands/generic-transformers'; export const SQUARE_SCRIPT = defineScript({ SCRIPT: 'return ARGV[1] * ARGV[1];', @@ -249,7 +251,7 @@ describe('Client', () => { testUtils.testWithClient('client.hGetAll should return object', async client => { await client.v4.hSet('key', 'field', 'value'); - + assert.deepEqual( await promisify(client.hGetAll).call(client, 'key'), Object.create(null, { @@ -351,7 +353,7 @@ describe('Client', () => { } }); - testUtils.testWithClient('client.multi.hGetAll should return object', async client => { + testUtils.testWithClient('client.multi.hGetAll should return object', async client => { assert.deepEqual( await multiExecAsync( client.multi() @@ -641,84 +643,188 @@ describe('Client', () => { return client.executeIsolated(isolated => killClient(isolated, client)); }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('scanIterator', async client => { - const promises = [], - keys = new Set(); - for (let i = 0; i < 100; i++) { - const key = i.toString(); - keys.add(key); - promises.push(client.set(key, '')); - } + describe('scanIterator', () => { + testUtils.testWithClient('strings', async client => { + const args: Array = [], + keys = new Set(); + for (let i = 0; i < 100; i++) { + const key = i.toString(); + args.push(key, ''); + keys.add(key); + } - await Promise.all(promises); + await client.mSet(args); - const results = new Set(); - for await (const key of client.scanIterator()) { - results.add(key); - } + const results = new Set(); + for await (const key of client.scanIterator()) { + results.add(key); + } - assert.deepEqual(keys, results); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(keys, results); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('hScanIterator', async client => { - const hash: Record = {}; - for (let i = 0; i < 100; i++) { - hash[i.toString()] = i.toString(); - } + testUtils.testWithClient('buffers', async client => { + const args: Array = [], + keys = new Set(); + for (let i = 0; i < 100; i++) { + const key = Buffer.from([i]); + args.push(key, ''); + keys.add(key); + } - await client.hSet('key', hash); + await client.mSet(args); - const results: Record = {}; - for await (const { field, value } of client.hScanIterator('key')) { - results[field] = value; - } + const results = new Set(), + iterator = client.scanIterator( + client.commandOptions({ returnBuffers: true }) + ); + for await (const key of iterator) { + results.add(key); + } - assert.deepEqual(hash, results); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(keys, results); + }, GLOBAL.SERVERS.OPEN); + }); - testUtils.testWithClient('sScanIterator', async client => { - const members = new Set(); - for (let i = 0; i < 100; i++) { - members.add(i.toString()); - } + describe('hScanIterator', () => { + testUtils.testWithClient('strings', async client => { + const hash: Record = {}; + for (let i = 0; i < 100; i++) { + hash[i.toString()] = i.toString(); + } - await client.sAdd('key', Array.from(members)); + await client.hSet('key', hash); - const results = new Set(); - for await (const key of client.sScanIterator('key')) { - results.add(key); - } + const results: Record = {}; + for await (const { field, value } of client.hScanIterator('key')) { + results[field] = value; + } - assert.deepEqual(members, results); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(hash, results); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('zScanIterator', async client => { - const members = []; - for (let i = 0; i < 100; i++) { - members.push({ - score: 1, - value: i.toString() - }); - } + testUtils.testWithClient('buffers', async client => { + const hash = new Map(); + for (let i = 0; i < 100; i++) { + const buffer = Buffer.from([i]); + hash.set(buffer, buffer); + } - await client.zAdd('key', members); + await client.hSet('key', hash); - const map = new Map(); - for await (const member of client.zScanIterator('key')) { - map.set(member.value, member.score); - } + const results = new Map(), + iterator = client.hScanIterator( + client.commandOptions({ returnBuffers: true }), + 'key' + ); + for await (const { field, value } of iterator) { + results.set(field, value); + } - type MemberTuple = [string, number]; + assert.deepEqual(hash, results); + }, GLOBAL.SERVERS.OPEN); + }); - function sort(a: MemberTuple, b: MemberTuple) { - return Number(b[0]) - Number(a[0]); - } + describe('sScanIterator', () => { + testUtils.testWithClient('strings', async client => { + const members = new Set(); + for (let i = 0; i < 100; i++) { + members.add(i.toString()); + } - assert.deepEqual( - [...map.entries()].sort(sort), - members.map(member => [member.value, member.score]).sort(sort) - ); - }, GLOBAL.SERVERS.OPEN); + await client.sAdd('key', Array.from(members)); + + const results = new Set(); + for await (const key of client.sScanIterator('key')) { + results.add(key); + } + + assert.deepEqual(members, results); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('buffers', async client => { + const members = new Set(); + for (let i = 0; i < 100; i++) { + members.add(Buffer.from([i])); + } + + await client.sAdd('key', Array.from(members)); + + const results = new Set(), + iterator = client.sScanIterator( + client.commandOptions({ returnBuffers: true }), + 'key' + ); + for await (const key of iterator) { + results.add(key); + } + + assert.deepEqual(members, results); + }, GLOBAL.SERVERS.OPEN); + }); + + describe('zScanIterator', () => { + testUtils.testWithClient('strings', async client => { + const members: Array> = []; + for (let i = 0; i < 100; i++) { + members.push({ + score: i, + value: i.toString() + }); + } + + await client.zAdd('key', members); + + const map = new Map(); + for await (const member of client.zScanIterator('key')) { + map.set(member.value, member.score); + } + + type MemberTuple = [string, number]; + + function sort(a: MemberTuple, b: MemberTuple) { + return Number(b[0]) - Number(a[0]); + } + + assert.deepEqual( + [...map.entries()].sort(sort), + members.map(member => [member.value, member.score]).sort(sort) + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('buffers', async client => { + const members: Array> = []; + for (let i = 0; i < 100; i++) { + members.push({ + score: i, + value: Buffer.from([i]) + }); + } + + await client.zAdd('key', members); + + const map = new Map(), + iterator = client.zScanIterator( + client.commandOptions({ returnBuffers: true }), + 'key' + ); + + for await (const member of iterator) { + map.set(member.value, member.score); + } + + type MemberTuple = [Buffer, number]; + + function sort(a: MemberTuple, b: MemberTuple) { + return b[0][0] - a[0][0]; + } + + assert.deepEqual( + [...map.entries()].sort(sort), + members.map(member => [member.value, member.score]).sort(sort) + ); + }, GLOBAL.SERVERS.OPEN); + }); describe('PubSub', () => { testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index b4bf49fc7bc..528200f7506 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,5 +1,5 @@ import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands'; +import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands, RedisCommandArgument } from '../commands'; import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket'; import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; @@ -460,7 +460,7 @@ export default class RedisClient< ); } else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) { return Promise.reject(new ClientOfflineError()); - } + } const promise = this.#queue.addCommand(args, options); this.#tick(); @@ -742,10 +742,43 @@ export default class RedisClient< return results; } - async* scanIterator(options?: ScanCommandOptions): AsyncIterable { + // #scanIterator>( + // commandOptions: T, + // options?: ScanCommandOptions + // ): AsyncIterable; + // #scanIterator( + // options?: ScanCommandOptions + // ): AsyncIterable; + // async* #scanIterator>( + // commandOptions?: T | ScanCommandOptions, + // options?: ScanCommandOptions + // ): AsyncIterable { + + // } + + scanIterator>( + commandOptions: T, + options?: ScanCommandOptions + ): AsyncIterable; + scanIterator( + options?: ScanCommandOptions + ): AsyncIterable; + async* scanIterator>( + commandOptions?: T | ScanCommandOptions, + options?: ScanCommandOptions + ): AsyncIterable { + if (!isCommandOptions(commandOptions)) { + options = commandOptions; + commandOptions = undefined; + } + + const scan = commandOptions ? + (...args: Array) => (this as any).scan(commandOptions, ...args) : + (this as any).scan.bind(this); + let cursor = 0; do { - const reply = await (this as any).scan(cursor, options); + const reply = await scan(cursor, options); cursor = reply.cursor; for (const key of reply.keys) { yield key; @@ -753,10 +786,33 @@ export default class RedisClient< } while (cursor !== 0); } - async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable> { + hScanIterator>( + commandOptions: T, + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable>; + hScanIterator( + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable>; + async* hScanIterator>( + commandOptions?: T | RedisCommandArgument, + key?: RedisCommandArgument | ScanOptions, + options?: ScanOptions + ): AsyncIterable> { + if (!isCommandOptions(commandOptions)) { + options = key as ScanOptions | undefined; + key = commandOptions; + commandOptions = undefined; + } + + const hScan = commandOptions ? + (...args: Array) => (this as any).hScan(commandOptions, ...args) : + (this as any).hScan.bind(this); + let cursor = 0; do { - const reply = await (this as any).hScan(key, cursor, options); + const reply = await hScan(key, cursor, options); cursor = reply.cursor; for (const tuple of reply.tuples) { yield tuple; @@ -764,10 +820,34 @@ export default class RedisClient< } while (cursor !== 0); } - async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable { + + sScanIterator>( + commandOptions: T, + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable; + sScanIterator( + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable; + async* sScanIterator>( + commandOptions?: T | RedisCommandArgument, + key?: RedisCommandArgument | ScanOptions, + options?: ScanOptions + ): AsyncIterable { + if (!isCommandOptions(commandOptions)) { + options = key as ScanOptions | undefined; + key = commandOptions; + commandOptions = undefined; + } + + const sScan = commandOptions ? + (...args: Array) => (this as any).sScan(commandOptions, ...args) : + (this as any).sScan.bind(this); + let cursor = 0; do { - const reply = await (this as any).sScan(key, cursor, options); + const reply = await sScan(key, cursor, options); cursor = reply.cursor; for (const member of reply.members) { yield member; @@ -775,10 +855,33 @@ export default class RedisClient< } while (cursor !== 0); } - async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable> { + zScanIterator>( + commandOptions: T, + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable>; + zScanIterator( + key: RedisCommandArgument, + options?: ScanOptions + ): AsyncIterable>; + async* zScanIterator>( + commandOptions?: T | RedisCommandArgument, + key?: RedisCommandArgument | ScanOptions, + options?: ScanOptions + ): AsyncIterable> { + if (!isCommandOptions(commandOptions)) { + options = key as ScanOptions | undefined; + key = commandOptions; + commandOptions = undefined; + } + + const zScan = commandOptions ? + (...args: Array) => (this as any).zScan(commandOptions, ...args) : + (this as any).zScan.bind(this); + let cursor = 0; do { - const reply = await (this as any).zScan(key, cursor, options); + const reply = await zScan(key, cursor, options); cursor = reply.cursor; for (const member of reply.members) { yield member;