diff --git a/packages/client/index.ts b/packages/client/index.ts index 177614c113..ffaf08d199 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -35,7 +35,7 @@ export const createSentinel = RedisSentinel.create; export { GEO_REPLY_WITH, GeoReplyWith } from './lib/commands/GEOSEARCH_WITH'; -export { SetOptions, CLIENT_KILL_FILTERS, CLIENT_UNBLOCK_MODES, FAILOVER_MODES, CLUSTER_SLOT_STATES, COMMAND_LIST_FILTER_BY, REDIS_FLUSH_MODES } from './lib/commands' +export { SetOptions, CLIENT_KILL_FILTERS, CLIENT_UNBLOCK_MODES, FAILOVER_MODES, CLUSTER_SLOT_STATES, COMMAND_LIST_FILTER_BY, REDIS_FLUSH_MODES, AR_PREDICATE_TYPES, AR_PREDICATE_COMBINATORS, AR_OPERATIONS } from './lib/commands' export { BasicClientSideCache, BasicPooledClientSideCache } from './lib/client/cache'; export { OpenTelemetry } from './lib/opentelemetry'; diff --git a/packages/client/lib/commands/ARCOUNT.spec.ts b/packages/client/lib/commands/ARCOUNT.spec.ts new file mode 100644 index 0000000000..090d18eff4 --- /dev/null +++ b/packages/client/lib/commands/ARCOUNT.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARCOUNT from './ARCOUNT'; +import { parseArgs } from './generic-transformers'; + +describe('ARCOUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARCOUNT, 'key'), + ['ARCOUNT', 'key'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arCount', async client => { + await client.arSet('key', 0, ['v0', 'v1', 'v2']); + assert.equal( + await client.arCount('key'), + 3 + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARCOUNT.ts b/packages/client/lib/commands/ARCOUNT.ts new file mode 100644 index 0000000000..301b675a3c --- /dev/null +++ b/packages/client/lib/commands/ARCOUNT.ts @@ -0,0 +1,11 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ARCOUNT'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARDEL.spec.ts b/packages/client/lib/commands/ARDEL.spec.ts new file mode 100644 index 0000000000..ad16219ae3 --- /dev/null +++ b/packages/client/lib/commands/ARDEL.spec.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARDEL from './ARDEL'; +import { parseArgs } from './generic-transformers'; + +describe('ARDEL', () => { + describe('transformArguments', () => { + it('single index', () => { + assert.deepEqual( + parseArgs(ARDEL, 'key', 0), + ['ARDEL', 'key', '0'] + ); + }); + + it('multiple indices', () => { + assert.deepEqual( + parseArgs(ARDEL, 'key', [0, 2, 4]), + ['ARDEL', 'key', '0', '2', '4'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDel single index', async client => { + assert.equal(await client.arSet('key', 0, ['a', 'b', 'c']), 3); + assert.equal(await client.arDel('key', 1), 1); + assert.equal(await client.arGet('key', 1), null); + assert.equal(await client.arCount('key'), 2); + // already deleted → 0 + assert.equal(await client.arDel('key', 1), 0); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDel multiple indices', async client => { + assert.equal(await client.arSet('key', 0, ['a', 'b', 'c', 'd']), 4); + assert.equal(await client.arDel('key', [0, 1, 2]), 3); + assert.equal(await client.arCount('key'), 1); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDel last element removes the key', async client => { + assert.equal(await client.arSet('key', 0, 'a'), 1); + assert.equal(await client.arDel('key', 0), 1); + assert.equal(await client.exists('key'), 0); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARDEL.ts b/packages/client/lib/commands/ARDEL.ts new file mode 100644 index 0000000000..99fa1fb9e1 --- /dev/null +++ b/packages/client/lib/commands/ARDEL.ts @@ -0,0 +1,17 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export type ArIndex = number | string; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, indices: ArIndex | Array) { + parser.push('ARDEL'); + parser.pushKey(key); + if (Array.isArray(indices)) { + for (const i of indices) parser.push(i.toString()); + } else { + parser.push(indices.toString()); + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARDELRANGE.spec.ts b/packages/client/lib/commands/ARDELRANGE.spec.ts new file mode 100644 index 0000000000..52af01ee2f --- /dev/null +++ b/packages/client/lib/commands/ARDELRANGE.spec.ts @@ -0,0 +1,45 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARDELRANGE from './ARDELRANGE'; +import { parseArgs } from './generic-transformers'; + +describe('ARDELRANGE', () => { + describe('transformArguments', () => { + it('single range', () => { + assert.deepEqual( + parseArgs(ARDELRANGE, 'key', [[0, 4]]), + ['ARDELRANGE', 'key', '0', '4'] + ); + }); + + it('multiple ranges', () => { + assert.deepEqual( + parseArgs(ARDELRANGE, 'key', [[0, 1], [3, 4]]), + ['ARDELRANGE', 'key', '0', '1', '3', '4'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDelRange single range', async client => { + for (let i = 0; i < 10; i++) await client.arSet('key', i, (i * 10).toString()); + assert.equal(await client.arCount('key'), 10); + assert.equal(await client.arDelRange('key', [[2, 6]]), 5); + assert.equal(await client.arCount('key'), 5); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDelRange reverse range', async client => { + for (let i = 0; i < 10; i++) await client.arSet('key', i, (i * 10).toString()); + // start > end still deletes the same 5 elements + assert.equal(await client.arDelRange('key', [[6, 2]]), 5); + assert.equal(await client.arCount('key'), 5); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arDelRange multiple ranges', async client => { + assert.equal(await client.arSet('key', 0, ['a', 'b', 'c', 'd', 'e', 'f']), 6); + assert.equal(await client.arDelRange('key', [[0, 1], [4, 5]]), 4); + assert.deepEqual( + await client.arGetRange('key', 0, 5), + [null, null, 'c', 'd', null, null] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARDELRANGE.ts b/packages/client/lib/commands/ARDELRANGE.ts new file mode 100644 index 0000000000..7f41d67d04 --- /dev/null +++ b/packages/client/lib/commands/ARDELRANGE.ts @@ -0,0 +1,20 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export type ArDelRangeRange = [start: number | string, end: number | string]; + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + ranges: Array + ) { + parser.push('ARDELRANGE'); + parser.pushKey(key); + + for (const [start, end] of ranges) { + parser.push(start.toString(), end.toString()); + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARGET.spec.ts b/packages/client/lib/commands/ARGET.spec.ts new file mode 100644 index 0000000000..d76a7f56d5 --- /dev/null +++ b/packages/client/lib/commands/ARGET.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARGET from './ARGET'; +import { parseArgs } from './generic-transformers'; + +describe('ARGET', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARGET, 'key', 0), + ['ARGET', 'key', '0'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGet', async client => { + await client.arSet('key', 0, 'v0'); + assert.equal( + await client.arGet('key', 0), + 'v0' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARGET.ts b/packages/client/lib/commands/ARGET.ts new file mode 100644 index 0000000000..6a563ce675 --- /dev/null +++ b/packages/client/lib/commands/ARGET.ts @@ -0,0 +1,12 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, index: number | string) { + parser.push('ARGET'); + parser.pushKey(key); + parser.push(index.toString()); + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARGETRANGE.spec.ts b/packages/client/lib/commands/ARGETRANGE.spec.ts new file mode 100644 index 0000000000..dd1c4da9f4 --- /dev/null +++ b/packages/client/lib/commands/ARGETRANGE.spec.ts @@ -0,0 +1,25 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARGETRANGE from './ARGETRANGE'; +import { parseArgs } from './generic-transformers'; + +describe('ARGETRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARGETRANGE, 'key', 0, 10), + ['ARGETRANGE', 'key', '0', '10'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGetRange forward + reverse', async client => { + assert.equal(await client.arSet('key', 0, ['a', 'b', 'c', 'd', 'e']), 5); + assert.deepEqual(await client.arGetRange('key', 1, 3), ['b', 'c', 'd']); + assert.deepEqual(await client.arGetRange('key', 3, 1), ['d', 'c', 'b']); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGetRange errors when range exceeds maximum', async client => { + assert.equal(await client.arSet('key', 0, ['a', 'b', 'c', 'd', 'e']), 5); + await assert.rejects(() => client.arGetRange('key', 0, 1_000_000), /range exceeds maximum/i); + await assert.rejects(() => client.arGetRange('key', 1_000_000, 0), /range exceeds maximum/i); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARGETRANGE.ts b/packages/client/lib/commands/ARGETRANGE.ts new file mode 100644 index 0000000000..800bbd6e21 --- /dev/null +++ b/packages/client/lib/commands/ARGETRANGE.ts @@ -0,0 +1,12 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, start: number | string, end: number | string) { + parser.push('ARGETRANGE'); + parser.pushKey(key); + parser.push(start.toString(), end.toString()); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARGREP.spec.ts b/packages/client/lib/commands/ARGREP.spec.ts new file mode 100644 index 0000000000..fb0edc4ae0 --- /dev/null +++ b/packages/client/lib/commands/ARGREP.spec.ts @@ -0,0 +1,191 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARGREP, { ArGrepPredicate } from './ARGREP'; +import { parseArgs } from './generic-transformers'; + +describe('ARGREP', () => { + describe('transformArguments', () => { + it('single predicate', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', 0, 10, [['EXACT', 'boot']]), + ['ARGREP', 'key', '0', '10', 'EXACT', 'boot'] + ); + }); + + it('multiple predicates', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', 0, 10, [['MATCH', 'warn'], ['MATCH', 'error']]), + ['ARGREP', 'key', '0', '10', 'MATCH', 'warn', 'MATCH', 'error'] + ); + }); + + it('with COMBINATOR', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', 0, 10, [['MATCH', 'a'], ['MATCH', 'b']], { COMBINATOR: 'AND' }), + ['ARGREP', 'key', '0', '10', 'MATCH', 'a', 'MATCH', 'b', 'AND'] + ); + }); + + it('with LIMIT, NOCASE', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', 0, 10, [['MATCH', 'error']], { + LIMIT: 5, + NOCASE: true + }), + ['ARGREP', 'key', '0', '10', 'MATCH', 'error', 'LIMIT', '5', 'NOCASE'] + ); + }); + + it('open-ended bounds (- and +)', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', '-', '+', [['EXACT', 'boot']]), + ['ARGREP', 'key', '-', '+', 'EXACT', 'boot'] + ); + }); + + it('mixed bounds (concrete + open)', () => { + assert.deepEqual( + parseArgs(ARGREP, 'key', 5, '+', [['EXACT', 'boot']]), + ['ARGREP', 'key', '5', '+', 'EXACT', 'boot'] + ); + assert.deepEqual( + parseArgs(ARGREP, 'key', '-', 10, [['EXACT', 'boot']]), + ['ARGREP', 'key', '-', '10', 'EXACT', 'boot'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep', async client => { + await client.arMSet('key', { 0: 'boot', 1: 'warn', 2: 'error', 3: 'boot' }); + assert.deepEqual( + await client.arGrep('key', 0, 3, [['EXACT', 'boot']]), + [0, 3] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep with open-ended bounds', async client => { + await client.arMSet('key', { 0: 'boot', 1: 'warn', 2: 'error', 3: 'boot' }); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['EXACT', 'boot']]), + [0, 3] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep with mixed bounds', async client => { + await client.arMSet('key', { 0: 'boot', 1: 'warn', 2: 'error', 3: 'boot' }); + assert.deepEqual( + await client.arGrep('key', 1, '+', [['EXACT', 'boot']]), + [3] + ); + assert.deepEqual( + await client.arGrep('key', '-', 2, [['EXACT', 'boot']]), + [0] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep MATCH on sparse', async client => { + await client.arMSet('key', [[0, 'alpha'], [1, 'beta'], [2, 'alphabet'], [5, 'gamma']]); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['MATCH', 'alpha']]), + [0, 2] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep AND + GLOB + NOCASE', async client => { + await client.arMSet('key', [ + [0, 'RedisArray'], + [1, 'redis-match'], + [2, 'array-only'], + [3, 'plain'] + ]); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['MATCH', 'redis'], ['GLOB', '*array*']], { + COMBINATOR: 'AND', + NOCASE: true + }), + [0] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep OR is the default combinator', async client => { + await client.arMSet('log', [ + [0, 'boot: ok'], + [1, 'warn: disk'], + [2, 'ERROR: cpu'], + [3, 'info: ready'], + [4, 'error: net'] + ]); + assert.deepEqual( + await client.arGrep('log', '-', '+', [['GLOB', 'warn:*'], ['GLOB', 'error:*']]), + [1, 4] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep is case-sensitive by default; NOCASE includes mixed-case matches', async client => { + await client.arMSet('log', [ + [0, 'boot: ok'], + [1, 'warn: disk'], + [2, 'ERROR: cpu'], + [3, 'info: ready'], + [4, 'error: net'] + ]); + assert.deepEqual( + await client.arGrep('log', '-', '+', [['GLOB', 'error:*']]), + [4] + ); + assert.deepEqual( + await client.arGrep('log', '-', '+', [['GLOB', 'error:*']], { NOCASE: true }), + [2, 4] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep LIMIT stops after N', async client => { + await client.arMSet('key', [[0, 'hit-1'], [1, 'hit-2'], [2, 'miss'], [3, 'hit-3']]); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['MATCH', 'hit']], { LIMIT: 2 }), + [0, 1] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep missing key returns empty', async client => { + assert.deepEqual( + await client.arGrep('missing', '-', '+', [['MATCH', 'foo']]), + [] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep RE basic match', async client => { + await client.arMSet('key', [[0, 'foo123'], [1, 'bar'], [2, 'zoo999'], [3, 'Foo777']]); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['RE', '^.*[0-9]{3}$']]), + [0, 2, 3] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep RE NOCASE', async client => { + await client.arMSet('key', [[0, 'foo123'], [1, 'bar'], [2, 'zoo999'], [3, 'Foo777']]); + assert.deepEqual( + await client.arGrep('key', '-', '+', [['RE', '^foo[0-9]+$']], { NOCASE: true }), + [0, 3] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep RE rejects oversize, backrefs, empty', async client => { + const re2048 = 'a'.repeat(2048); + const re2049 = 'a'.repeat(2049); + assert.equal(await client.arSet('key', 0, re2048), 1); + assert.deepEqual(await client.arGrep('key', '-', '+', [['RE', re2048]]), [0]); + await assert.rejects(() => client.arGrep('key', '-', '+', [['RE', re2049]]), /maximum is 2048 bytes/i); + await assert.rejects(() => client.arGrep('key', '-', '+', [['RE', '(a)\\1']]), /backreferences are not supported/i); + await assert.rejects(() => client.arGrep('key', '-', '+', [['RE', '']]), /regular expression is empty/i); + await assert.rejects(() => client.arGrep('key', '-', '+', [['RE', '\\x{1']]), /invalid regular expression/i); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrep enforces predicate limit (250 ok, 251 errors)', async client => { + assert.equal(await client.arSet('key', 0, 'foo'), 1); + const preds250: Array = Array.from({ length: 250 }, () => ['MATCH', 'foo']); + assert.deepEqual(await client.arGrep('key', '-', '+', preds250), [0]); + const preds251: Array = Array.from({ length: 251 }, () => ['MATCH', 'foo']); + await assert.rejects(() => client.arGrep('key', '-', '+', preds251), /maximum is 250/i); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARGREP.ts b/packages/client/lib/commands/ARGREP.ts new file mode 100644 index 0000000000..decbb5fb39 --- /dev/null +++ b/packages/client/lib/commands/ARGREP.ts @@ -0,0 +1,76 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; +import { Tail } from './generic-transformers'; + +export const AR_PREDICATE_TYPES = { + EXACT: 'EXACT', + MATCH: 'MATCH', + GLOB: 'GLOB', + RE: 'RE' +} as const; + +export type ArPredicateType = typeof AR_PREDICATE_TYPES[keyof typeof AR_PREDICATE_TYPES]; + +export const AR_PREDICATE_COMBINATORS = { + AND: 'AND', + OR: 'OR' +} as const; + +export type ArPredicateCombinator = typeof AR_PREDICATE_COMBINATORS[keyof typeof AR_PREDICATE_COMBINATORS]; + +export type ArGrepPredicate = [type: ArPredicateType, value: RedisArgument]; + +/** + * Bound for an ARGREP range: a numeric index, the literal `'-'` (open-ended + * lower bound), the literal `'+'` (open-ended upper bound), or a decimal + * string (use this when an index would exceed `Number.MAX_SAFE_INTEGER`). + */ +export type ArGrepBound = number | '-' | '+' | string; + +export interface ArGrepOptions { + COMBINATOR?: ArPredicateCombinator; + LIMIT?: number; + NOCASE?: boolean; +} + +export function parseArGrepArguments( + parser: CommandParser, + key: RedisArgument, + start: ArGrepBound, + end: ArGrepBound, + predicates: Array, + options?: ArGrepOptions +) { + parser.pushKey(key); + parser.push( + typeof start === 'number' ? start.toString() : start, + typeof end === 'number' ? end.toString() : end + ); + + for (const [type, value] of predicates) { + parser.push(type, value); + } + + if (options?.COMBINATOR !== undefined) { + parser.push(options.COMBINATOR); + } + + if (options?.LIMIT !== undefined) { + parser.push('LIMIT', options.LIMIT.toString()); + } + + if (options?.NOCASE) { + parser.push('NOCASE'); + } +} + +export type ArGrepArguments = Tail>; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, ...args: ArGrepArguments) { + parser.push('ARGREP'); + parseArGrepArguments(parser, ...args); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARGREP_WITHVALUES.spec.ts b/packages/client/lib/commands/ARGREP_WITHVALUES.spec.ts new file mode 100644 index 0000000000..d06d8562e8 --- /dev/null +++ b/packages/client/lib/commands/ARGREP_WITHVALUES.spec.ts @@ -0,0 +1,61 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARGREP_WITHVALUES from './ARGREP_WITHVALUES'; +import { parseArgs } from './generic-transformers'; + +describe('ARGREP WITHVALUES', () => { + describe('transformArguments', () => { + it('single predicate', () => { + assert.deepEqual( + parseArgs(ARGREP_WITHVALUES, 'key', 0, 10, [['EXACT', 'boot']]), + ['ARGREP', 'key', '0', '10', 'EXACT', 'boot', 'WITHVALUES'] + ); + }); + + it('with options', () => { + assert.deepEqual( + parseArgs(ARGREP_WITHVALUES, 'key', 0, 10, [['MATCH', 'a'], ['MATCH', 'b']], { + COMBINATOR: 'AND', + LIMIT: 5, + NOCASE: true + }), + ['ARGREP', 'key', '0', '10', 'MATCH', 'a', 'MATCH', 'b', 'AND', 'LIMIT', '5', 'NOCASE', 'WITHVALUES'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrepWithValues', async client => { + await client.arMSet('key', { 0: 'boot', 1: 'warn', 2: 'error', 3: 'boot' }); + assert.deepEqual( + await client.arGrepWithValues('key', 0, 3, [['EXACT', 'boot']]), + [ + { index: 0, value: 'boot' }, + { index: 3, value: 'boot' } + ] + ); + }, GLOBAL.SERVERS.OPEN); + + // Confirms the server accepts our emitted ordering of trailing modifiers: + // COMBINATOR LIMIT N NOCASE WITHVALUES + // .NET emits AND/NOCASE/WITHVALUES/LIMIT instead; tcl claims order-insensitive. + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arGrepWithValues with COMBINATOR + LIMIT + NOCASE', async client => { + await client.arMSet('key', { + 0: 'RedisArray', + 1: 'redis-match', + 2: 'array-only', + 3: 'plain', + 4: 'redis-array-extra' + }); + const result = await client.arGrepWithValues( + 'key', + 0, + 4, + [['MATCH', 'redis'], ['GLOB', '*array*']], + { COMBINATOR: 'AND', LIMIT: 5, NOCASE: true } + ); + assert.deepEqual(result, [ + { index: 0, value: 'RedisArray' }, + { index: 4, value: 'redis-array-extra' } + ]); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARGREP_WITHVALUES.ts b/packages/client/lib/commands/ARGREP_WITHVALUES.ts new file mode 100644 index 0000000000..e27aa46ff8 --- /dev/null +++ b/packages/client/lib/commands/ARGREP_WITHVALUES.ts @@ -0,0 +1,24 @@ +import { CommandParser } from '../client/parser'; +import { ArrayReply, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '../RESP/types'; +import { parseArGrepArguments, ArGrepArguments } from './ARGREP'; + +export type ArGrepWithValuesReply = Array<{ + index: NumberReply; + value: BlobStringReply; +}>; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, ...args: ArGrepArguments) { + parser.push('ARGREP'); + parseArGrepArguments(parser, ...args); + parser.push('WITHVALUES'); + }, + transformReply: (reply: ArrayReply>) => { + const unwrapped = reply as unknown as UnwrapReply; + return unwrapped.map(pair => { + const [index, value] = pair as unknown as UnwrapReply; + return { index, value }; + }) satisfies ArGrepWithValuesReply; + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARINFO.spec.ts b/packages/client/lib/commands/ARINFO.spec.ts new file mode 100644 index 0000000000..0f1965326a --- /dev/null +++ b/packages/client/lib/commands/ARINFO.spec.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARINFO from './ARINFO'; +import { parseArgs } from './generic-transformers'; + +describe('ARINFO', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(ARINFO, 'key'), + ['ARINFO', 'key'] + ); + }); + + it('with FULL', () => { + assert.deepEqual( + parseArgs(ARINFO, 'key', { FULL: true }), + ['ARINFO', 'key', 'FULL'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arInfo returns expected fields', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [1, 'b'], [100, 'c']]), 3); + const info = await client.arInfo('key') as Record; + assert.equal(info['count'], 3); + assert.equal(info['len'], 101); + assert.equal(info['next-insert-index'], 0); + assert.equal(info['slices'], 1); + assert.equal(info['directory-size'], 1); + assert.equal(info['super-dir-entries'], 0); + assert.equal(info['slice-size'], 4096); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arInfo FULL returns base fields plus extra detail', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [1, 'b'], [100, 'c']]), 3); + + const info = await client.arInfo('key') as Record; + const fullInfo = await client.arInfo('key', { FULL: true }) as Record; + + // base fields are present and unchanged under FULL + for (const field of Object.keys(info)) { + assert.deepEqual(fullInfo[field], info[field], `field "${field}" should match base arInfo`); + } + + // FULL adds per-slice statistics. With 3 entries spanning index 0..100 + // the storage uses a single sparse slice. + assert.equal(fullInfo['dense-slices'], 0); + assert.equal(fullInfo['sparse-slices'], 1); + for (const field of ['avg-dense-size', 'avg-dense-fill', 'avg-sparse-size']) { + assert.ok(field in fullInfo, `FULL should include "${field}"`); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'array TYPE and OBJECT ENCODING', async client => { + assert.equal(await client.arSet('key', 0, 'hello'), 1); + assert.equal(await client.type('key'), 'array'); + assert.equal(await client.objectEncoding('key'), 'sliced-array'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'AR* commands reject non-array keys (WRONGTYPE)', async client => { + await client.set('key', 'value'); + await assert.rejects(() => client.arGet('key', 0), /WRONGTYPE/i); + await assert.rejects(() => client.arSet('key', 0, 'foo'), /WRONGTYPE/i); + await assert.rejects(() => client.arLen('key'), /WRONGTYPE/i); + await assert.rejects(() => client.arCount('key'), /WRONGTYPE/i); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARINFO.ts b/packages/client/lib/commands/ARINFO.ts new file mode 100644 index 0000000000..1c3e78bf00 --- /dev/null +++ b/packages/client/lib/commands/ARINFO.ts @@ -0,0 +1,25 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, MapReply, BlobStringReply, NumberReply, DoubleReply, ArrayReply, Command } from '../RESP/types'; +import { transformTuplesReply } from './generic-transformers'; + +export interface ArInfoOptions { + FULL?: boolean; +} + +export type ArInfoReply = MapReply; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, options?: ArInfoOptions) { + parser.push('ARINFO'); + parser.pushKey(key); + if (options?.FULL) { + parser.push('FULL'); + } + }, + transformReply: { + 2: (reply: ArrayReply, preserve, typeMapping) => + transformTuplesReply(reply as unknown as ArrayReply, preserve, typeMapping) as unknown as ArInfoReply, + 3: undefined as unknown as () => ArInfoReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARINSERT.spec.ts b/packages/client/lib/commands/ARINSERT.spec.ts new file mode 100644 index 0000000000..50c08add0c --- /dev/null +++ b/packages/client/lib/commands/ARINSERT.spec.ts @@ -0,0 +1,39 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARINSERT from './ARINSERT'; +import { parseArgs } from './generic-transformers'; + +describe('ARINSERT', () => { + describe('transformArguments', () => { + it('single value', () => { + assert.deepEqual( + parseArgs(ARINSERT, 'key', 'v0'), + ['ARINSERT', 'key', 'v0'] + ); + }); + + it('multiple values', () => { + assert.deepEqual( + parseArgs(ARINSERT, 'key', ['v0', 'v1', 'v2']), + ['ARINSERT', 'key', 'v0', 'v1', 'v2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arInsert advances write head', async client => { + assert.equal(await client.arInsert('key', 'a'), 0); + assert.equal(await client.arInsert('key', 'b'), 1); + assert.equal(await client.arInsert('key', 'c'), 2); + assert.equal(await client.arGet('key', 0), 'a'); + assert.equal(await client.arGet('key', 1), 'b'); + assert.equal(await client.arGet('key', 2), 'c'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arInsert overflows when cursor at MaxValue', async client => { + assert.equal(await client.arInsert('key', 'a'), 0); + // seek to ulong MaxValue + assert.equal(await client.arSeek('key', '18446744073709551615'), 1); + assert.equal(await client.arNext('key'), null); + await assert.rejects(() => client.arInsert('key', 'b'), /insert index overflow/i); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARINSERT.ts b/packages/client/lib/commands/ARINSERT.ts new file mode 100644 index 0000000000..71aeee7da0 --- /dev/null +++ b/packages/client/lib/commands/ARINSERT.ts @@ -0,0 +1,12 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, values: RedisVariadicArgument) { + parser.push('ARINSERT'); + parser.pushKey(key); + parser.pushVariadic(values); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARLASTITEMS.spec.ts b/packages/client/lib/commands/ARLASTITEMS.spec.ts new file mode 100644 index 0000000000..2f8415a2dd --- /dev/null +++ b/packages/client/lib/commands/ARLASTITEMS.spec.ts @@ -0,0 +1,49 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARLASTITEMS from './ARLASTITEMS'; +import { parseArgs } from './generic-transformers'; + +describe('ARLASTITEMS', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(ARLASTITEMS, 'key', 3), + ['ARLASTITEMS', 'key', '3'] + ); + }); + + it('with REV', () => { + assert.deepEqual( + parseArgs(ARLASTITEMS, 'key', 3, { REV: true }), + ['ARLASTITEMS', 'key', '3', 'REV'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arLastItems', async client => { + await client.arInsert('key', ['v0', 'v1', 'v2', 'v3', 'v4']); + assert.deepEqual( + await client.arLastItems('key', 3), + ['v2', 'v3', 'v4'] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arLastItems REV', async client => { + await client.arInsert('key', ['v0', 'v1', 'v2', 'v3', 'v4']); + assert.deepEqual( + await client.arLastItems('key', 3, { REV: true }), + ['v4', 'v3', 'v2'] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arLastItems still references tail after seek 0', async client => { + for (let i = 0; i < 5; i++) await client.arInsert('key', (i * 10).toString()); + assert.deepEqual(await client.arLastItems('key', 3), ['20', '30', '40']); + assert.deepEqual(await client.arLastItems('key', 3, { REV: true }), ['40', '30', '20']); + + // Seek to 0 rewinds the write cursor — but ARLASTITEMS still references the tail. + assert.equal(await client.arSeek('key', 0), 1); + assert.deepEqual(await client.arLastItems('key', 3), ['20', '30', '40']); + assert.deepEqual(await client.arLastItems('key', 3, { REV: true }), ['40', '30', '20']); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARLASTITEMS.ts b/packages/client/lib/commands/ARLASTITEMS.ts new file mode 100644 index 0000000000..c617a723cf --- /dev/null +++ b/packages/client/lib/commands/ARLASTITEMS.ts @@ -0,0 +1,25 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export interface ArLastItemsOptions { + REV?: boolean; +} + +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + count: number, + options?: ArLastItemsOptions + ) { + parser.push('ARLASTITEMS'); + parser.pushKey(key); + parser.push(count.toString()); + + if (options?.REV) { + parser.push('REV'); + } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARLEN.spec.ts b/packages/client/lib/commands/ARLEN.spec.ts new file mode 100644 index 0000000000..0c532e263e --- /dev/null +++ b/packages/client/lib/commands/ARLEN.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARLEN from './ARLEN'; +import { parseArgs } from './generic-transformers'; + +describe('ARLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARLEN, 'key'), + ['ARLEN', 'key'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arLen/arCount with sparse gaps', async client => { + // missing key → 0 for both + assert.equal(await client.arLen('key'), 0); + assert.equal(await client.arCount('key'), 0); + + // length = highest_index + 1; count = filled cells + assert.equal(await client.arSet('key', 0, 'a'), 1); + assert.equal(await client.arLen('key'), 1); + assert.equal(await client.arCount('key'), 1); + + assert.equal(await client.arSet('key', 5, 'b'), 1); + assert.equal(await client.arLen('key'), 6); + assert.equal(await client.arCount('key'), 2); + + assert.equal(await client.arSet('key', 100, 'c'), 1); + assert.equal(await client.arLen('key'), 101); + assert.equal(await client.arCount('key'), 3); + + // very wide sparse gaps + await client.del('key'); + await client.arSet('key', 0, 'a'); + await client.arSet('key', 10000, 'b'); + await client.arSet('key', 1000000, 'c'); + + assert.equal(await client.arGet('key', 0), 'a'); + assert.equal(await client.arGet('key', 10000), 'b'); + assert.equal(await client.arGet('key', 1000000), 'c'); + assert.equal(await client.arCount('key'), 3); + assert.equal(await client.arLen('key'), 1000001); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARLEN.ts b/packages/client/lib/commands/ARLEN.ts new file mode 100644 index 0000000000..eaa8a7b5be --- /dev/null +++ b/packages/client/lib/commands/ARLEN.ts @@ -0,0 +1,11 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ARLEN'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARMGET.spec.ts b/packages/client/lib/commands/ARMGET.spec.ts new file mode 100644 index 0000000000..c007922c01 --- /dev/null +++ b/packages/client/lib/commands/ARMGET.spec.ts @@ -0,0 +1,31 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARMGET from './ARMGET'; +import { parseArgs } from './generic-transformers'; + +describe('ARMGET', () => { + describe('transformArguments', () => { + it('single index', () => { + assert.deepEqual( + parseArgs(ARMGET, 'key', 0), + ['ARMGET', 'key', '0'] + ); + }); + + it('multiple indices', () => { + assert.deepEqual( + parseArgs(ARMGET, 'key', [0, 2, 4]), + ['ARMGET', 'key', '0', '2', '4'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arMGet mixed hits + nulls', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [1, 'b'], [5, 'c']]), 3); + // request 0, 1, 5, 3 → expect a, b, c, null + assert.deepEqual( + await client.arMGet('key', [0, 1, 5, 3]), + ['a', 'b', 'c', null] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARMGET.ts b/packages/client/lib/commands/ARMGET.ts new file mode 100644 index 0000000000..900d58861c --- /dev/null +++ b/packages/client/lib/commands/ARMGET.ts @@ -0,0 +1,16 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, indices: number | string | Array) { + parser.push('ARMGET'); + parser.pushKey(key); + if (Array.isArray(indices)) { + for (const i of indices) parser.push(i.toString()); + } else { + parser.push(indices.toString()); + } + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARMSET.spec.ts b/packages/client/lib/commands/ARMSET.spec.ts new file mode 100644 index 0000000000..0c0ee18412 --- /dev/null +++ b/packages/client/lib/commands/ARMSET.spec.ts @@ -0,0 +1,45 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARMSET from './ARMSET'; +import { parseArgs } from './generic-transformers'; + +describe('ARMSET', () => { + describe('transformArguments', () => { + it('object', () => { + assert.deepEqual( + parseArgs(ARMSET, 'key', { 0: 'v0', 2: 'v2', 4: 'v4' }), + ['ARMSET', 'key', '0', 'v0', '2', 'v2', '4', 'v4'] + ); + }); + + it('Map', () => { + assert.deepEqual( + parseArgs(ARMSET, 'key', new Map([[0, 'v0'], [2, 'v2']])), + ['ARMSET', 'key', '0', 'v0', '2', 'v2'] + ); + }); + + it('tuples', () => { + assert.deepEqual( + parseArgs(ARMSET, 'key', [[0, 'v0'], [2, 'v2']]), + ['ARMSET', 'key', '0', 'v0', '2', 'v2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arMSet returns newly-filled count', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [1, 'b'], [2, 'c']]), 3); + assert.equal(await client.arGet('key', 0), 'a'); + assert.equal(await client.arGet('key', 1), 'b'); + assert.equal(await client.arGet('key', 2), 'c'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arMSet only counts newly-filled cells', async client => { + // pre-fill index 0 + assert.equal(await client.arSet('key', 0, 'a'), 1); + // overwrite 0 (not new) + write new 1 → count of newly filled = 1 + assert.equal(await client.arMSet('key', [[0, 'aa'], [1, 'b']]), 1); + assert.equal(await client.arGet('key', 0), 'aa'); + assert.equal(await client.arGet('key', 1), 'b'); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARMSET.ts b/packages/client/lib/commands/ARMSET.ts new file mode 100644 index 0000000000..651667bd9e --- /dev/null +++ b/packages/client/lib/commands/ARMSET.ts @@ -0,0 +1,32 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export type ArMSetObject = Record; + +export type ArMSetMap = Map; + +export type ArMSetTuples = Array<[number | string, RedisArgument]>; + +export type ArMSetEntries = ArMSetObject | ArMSetMap | ArMSetTuples; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, entries: ArMSetEntries) { + parser.push('ARMSET'); + parser.pushKey(key); + + if (entries instanceof Map) { + for (const [index, value] of entries.entries()) { + parser.push(index.toString(), value); + } + } else if (Array.isArray(entries)) { + for (const [index, value] of entries) { + parser.push(index.toString(), value); + } + } else { + for (const index of Object.keys(entries)) { + parser.push(index, entries[index]); + } + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARNEXT.spec.ts b/packages/client/lib/commands/ARNEXT.spec.ts new file mode 100644 index 0000000000..7b92a3b11b --- /dev/null +++ b/packages/client/lib/commands/ARNEXT.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARNEXT from './ARNEXT'; +import { parseArgs } from './generic-transformers'; + +describe('ARNEXT', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(ARNEXT, 'key'), + ['ARNEXT', 'key'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arNext tracks insert cursor', async client => { + // empty key starts at 0 + assert.equal(await client.arNext('key'), 0); + assert.equal(await client.arInsert('key', 'a'), 0); + assert.equal(await client.arNext('key'), 1); + assert.equal(await client.arInsert('key', 'b'), 1); + assert.equal(await client.arNext('key'), 2); + // after seek, ARNEXT reflects the new cursor + assert.equal(await client.arSeek('key', 10), 1); + assert.equal(await client.arInsert('key', 'c'), 10); + assert.equal(await client.arNext('key'), 11); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARNEXT.ts b/packages/client/lib/commands/ARNEXT.ts new file mode 100644 index 0000000000..92e74f7b9b --- /dev/null +++ b/packages/client/lib/commands/ARNEXT.ts @@ -0,0 +1,11 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ARNEXT'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/AROP.spec.ts b/packages/client/lib/commands/AROP.spec.ts new file mode 100644 index 0000000000..30bf915ce6 --- /dev/null +++ b/packages/client/lib/commands/AROP.spec.ts @@ -0,0 +1,67 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import AROP from './AROP'; +import { parseArgs } from './generic-transformers'; + +describe('AROP', () => { + describe('transformArguments', () => { + it('without value', () => { + assert.deepEqual( + parseArgs(AROP, 'key', 0, 4, 'SUM'), + ['AROP', 'key', '0', '4', 'SUM'] + ); + }); + + it('with value (MATCH)', () => { + assert.deepEqual( + parseArgs(AROP, 'key', 0, 4, 'MATCH', 2), + ['AROP', 'key', '0', '4', 'MATCH', '2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp SUM', async client => { + assert.equal(await client.arMSet('key', [[0, '10'], [1, '20'], [2, '30']]), 3); + assert.equal(await client.arOp('key', 0, 2, 'SUM'), '60'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp MIN/MAX', async client => { + assert.equal(await client.arMSet('key', [[0, '30'], [1, '10'], [2, '20']]), 3); + assert.equal(await client.arOp('key', 0, 2, 'MIN'), '10'); + assert.equal(await client.arOp('key', 0, 2, 'MAX'), '30'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp MATCH counts occurrences', async client => { + assert.equal(await client.arMSet('key', [[0, 'hello'], [1, 'world'], [2, 'hello'], [3, 'foo']]), 4); + assert.equal(await client.arOp('key', 0, 3, 'MATCH', 'hello'), 2); + assert.equal(await client.arOp('key', 0, 3, 'MATCH', 'world'), 1); + assert.equal(await client.arOp('key', 0, 3, 'MATCH', 'bar'), 0); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp USED counts filled cells in range', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [2, 'b'], [5, 'c']]), 3); + assert.equal(await client.arOp('key', 0, 10, 'USED'), 3); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp bitwise AND/OR/XOR on integers', async client => { + assert.equal(await client.arMSet('key', [[0, '255'], [1, '15'], [2, '240']]), 3); + assert.equal(await client.arOp('key', 0, 2, 'AND'), 0); + assert.equal(await client.arOp('key', 0, 2, 'OR'), 255); + assert.equal(await client.arOp('key', 0, 2, 'XOR'), 0); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp bitwise truncates floats toward zero', async client => { + assert.equal(await client.arMSet('key', [[0, '7.9'], [1, '3.2'], [2, '1.8']]), 3); + assert.equal(await client.arOp('key', 0, 2, 'AND'), 1); // 7 & 3 & 1 + assert.equal(await client.arOp('key', 0, 2, 'OR'), 7); // 7 | 3 | 1 + assert.equal(await client.arOp('key', 0, 2, 'XOR'), 5); // 7 ^ 3 ^ 1 + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arOp server validates operand presence', async client => { + assert.equal(await client.arMSet('key', [[0, '10'], [1, '20']]), 2); + // MATCH requires an operand + await assert.rejects(() => client.arOp('key', 0, 1, 'MATCH')); + // SUM rejects an operand + await assert.rejects(() => client.arOp('key', 0, 1, 'SUM', 'value')); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/AROP.ts b/packages/client/lib/commands/AROP.ts new file mode 100644 index 0000000000..3716f75b9a --- /dev/null +++ b/packages/client/lib/commands/AROP.ts @@ -0,0 +1,36 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, NumberReply, NullReply, Command } from '../RESP/types'; + +export const AR_OPERATIONS = { + SUM: 'SUM', + MIN: 'MIN', + MAX: 'MAX', + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + MATCH: 'MATCH', + USED: 'USED' +} as const; + +export type ArOperation = typeof AR_OPERATIONS[keyof typeof AR_OPERATIONS]; + +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + start: number | string, + end: number | string, + operation: ArOperation, + value?: RedisArgument | number + ) { + parser.push('AROP'); + parser.pushKey(key); + parser.push(start.toString(), end.toString(), operation); + + if (value !== undefined) { + parser.push(typeof value === 'number' ? value.toString() : value); + } + }, + transformReply: undefined as unknown as () => BlobStringReply | NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARRING.spec.ts b/packages/client/lib/commands/ARRING.spec.ts new file mode 100644 index 0000000000..ac9f5df1c3 --- /dev/null +++ b/packages/client/lib/commands/ARRING.spec.ts @@ -0,0 +1,50 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARRING from './ARRING'; +import { parseArgs } from './generic-transformers'; + +describe('ARRING', () => { + describe('transformArguments', () => { + it('single value', () => { + assert.deepEqual( + parseArgs(ARRING, 'key', 3, 'v0'), + ['ARRING', 'key', '3', 'v0'] + ); + }); + + it('multiple values', () => { + assert.deepEqual( + parseArgs(ARRING, 'key', 4, ['v0', 'v1', 'v2']), + ['ARRING', 'key', '4', 'v0', 'v1', 'v2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arRing', async client => { + assert.equal( + await client.arRing('key', 4, ['v0', 'v1', 'v2']), + 2 + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arRing wraps when full', async client => { + await client.arRing('key', 3, ['v0', 'v1', 'v2']); + assert.equal( + await client.arRing('key', 3, 'v3'), + 0 + ); + assert.equal(await client.arGet('key', 0), 'v3'); + assert.equal(await client.arGet('key', 1), 'v1'); + assert.equal(await client.arGet('key', 2), 'v2'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arRing keeps last `maxLength` values', async client => { + for (let i = 0; i < 10; i++) await client.arRing('key', 5, i.toString()); + assert.equal(await client.arGet('key', 0), '5'); + assert.equal(await client.arGet('key', 1), '6'); + assert.equal(await client.arGet('key', 2), '7'); + assert.equal(await client.arGet('key', 3), '8'); + assert.equal(await client.arGet('key', 4), '9'); + assert.equal(await client.arCount('key'), 5); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARRING.ts b/packages/client/lib/commands/ARRING.ts new file mode 100644 index 0000000000..9c5432030c --- /dev/null +++ b/packages/client/lib/commands/ARRING.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + size: number | string, + values: RedisVariadicArgument + ) { + parser.push('ARRING'); + parser.pushKey(key); + parser.push(size.toString()); + parser.pushVariadic(values); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARSCAN.spec.ts b/packages/client/lib/commands/ARSCAN.spec.ts new file mode 100644 index 0000000000..085aecd802 --- /dev/null +++ b/packages/client/lib/commands/ARSCAN.spec.ts @@ -0,0 +1,57 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARSCAN from './ARSCAN'; +import { parseArgs } from './generic-transformers'; + +describe('ARSCAN', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(ARSCAN, 'key', 0, 10), + ['ARSCAN', 'key', '0', '10'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + parseArgs(ARSCAN, 'key', 0, 10, { LIMIT: 2 }), + ['ARSCAN', 'key', '0', '10', 'LIMIT', '2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arScan sparse', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [5, 'b'], [9, 'c']]), 3); + assert.deepEqual(await client.arScan('key', 0, 10), [ + { index: 0, value: 'a' }, + { index: 5, value: 'b' }, + { index: 9, value: 'c' } + ]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arScan empty range returns empty array', async client => { + assert.equal(await client.arSet('key', 500, 'x'), 1); + assert.deepEqual(await client.arScan('key', 0, 100), []); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arScan reverse range', async client => { + assert.equal(await client.arMSet('key', [[0, 'a'], [5, 'b']]), 2); + assert.deepEqual(await client.arScan('key', 5, 0), [ + { index: 5, value: 'b' }, + { index: 0, value: 'a' } + ]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arScan missing key returns empty', async client => { + assert.deepEqual(await client.arScan('missing', 0, 100), []); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arScan mixed value types stringify', async client => { + assert.equal(await client.arMSet('key', [[0, 'string'], [1, '12345'], [2, '3.14']]), 3); + assert.deepEqual(await client.arScan('key', 0, 10), [ + { index: 0, value: 'string' }, + { index: 1, value: '12345' }, + { index: 2, value: '3.14' } + ]); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARSCAN.ts b/packages/client/lib/commands/ARSCAN.ts new file mode 100644 index 0000000000..765e86eebd --- /dev/null +++ b/packages/client/lib/commands/ARSCAN.ts @@ -0,0 +1,37 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '../RESP/types'; + +export interface ArScanOptions { + LIMIT?: number; +} + +export type ArScanReply = Array<{ + index: NumberReply; + value: BlobStringReply; +}>; + +export default { + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + key: RedisArgument, + start: number | string, + end: number | string, + options?: ArScanOptions + ) { + parser.push('ARSCAN'); + parser.pushKey(key); + parser.push(start.toString(), end.toString()); + + if (options?.LIMIT !== undefined) { + parser.push('LIMIT', options.LIMIT.toString()); + } + }, + transformReply: (reply: ArrayReply>) => { + const unwrapped = reply as unknown as UnwrapReply; + return unwrapped.map(pair => { + const [index, value] = pair as unknown as UnwrapReply; + return { index, value }; + }) satisfies ArScanReply; + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARSEEK.spec.ts b/packages/client/lib/commands/ARSEEK.spec.ts new file mode 100644 index 0000000000..6b09c8114b --- /dev/null +++ b/packages/client/lib/commands/ARSEEK.spec.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARSEEK from './ARSEEK'; +import { parseArgs } from './generic-transformers'; + +describe('ARSEEK', () => { + describe('transformArguments', () => { + it('number index', () => { + assert.deepEqual( + parseArgs(ARSEEK, 'key', 10), + ['ARSEEK', 'key', '10'] + ); + }); + + it('string index for >Number.MAX_SAFE_INTEGER', () => { + assert.deepEqual( + parseArgs(ARSEEK, 'key', '18446744073709551614'), + ['ARSEEK', 'key', '18446744073709551614'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arSeek', async client => { + await client.arInsert('key', 'v0'); + assert.equal( + await client.arSeek('key', 10), + 1 + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arSeek with string index >2^53', async client => { + await client.arInsert('key', 'v0'); + // 2^54 - 1, exceeds Number.MAX_SAFE_INTEGER; must be passed as a string + assert.equal( + await client.arSeek('key', '18014398509481983'), + 1 + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arSeek on missing key returns 0', async client => { + assert.equal(await client.arSeek('missing', 10), 0); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARSEEK.ts b/packages/client/lib/commands/ARSEEK.ts new file mode 100644 index 0000000000..ef58957ad2 --- /dev/null +++ b/packages/client/lib/commands/ARSEEK.ts @@ -0,0 +1,11 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, index: number | string) { + parser.push('ARSEEK'); + parser.pushKey(key); + parser.push(index.toString()); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ARSET.spec.ts b/packages/client/lib/commands/ARSET.spec.ts new file mode 100644 index 0000000000..d1a82d9335 --- /dev/null +++ b/packages/client/lib/commands/ARSET.spec.ts @@ -0,0 +1,63 @@ +import { strict as assert } from 'node:assert'; +import { randomBytes } from 'node:crypto'; +import testUtils, { GLOBAL } from '../test-utils'; +import ARSET from './ARSET'; +import { RESP_TYPES } from '../RESP/decoder'; +import { parseArgs } from './generic-transformers'; + +describe('ARSET', () => { + describe('transformArguments', () => { + it('single value', () => { + assert.deepEqual( + parseArgs(ARSET, 'key', 0, 'v0'), + ['ARSET', 'key', '0', 'v0'] + ); + }); + + it('multiple values', () => { + assert.deepEqual( + parseArgs(ARSET, 'key', 0, ['v0', 'v1', 'v2']), + ['ARSET', 'key', '0', 'v0', 'v1', 'v2'] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arSet/arGet basics', async client => { + // first set returns 1 (newly filled), get returns the value, unset index returns null + assert.equal(await client.arSet('key', 0, 'hello'), 1); + assert.equal(await client.arGet('key', 0), 'hello'); + assert.equal(await client.arGet('key', 1), null); + + // overwrite returns 0 (already filled) + assert.equal(await client.arSet('key', 0, 'world'), 0); + assert.equal(await client.arGet('key', 0), 'world'); + + // missing key + assert.equal(await client.arGet('missing', 0), null); + + // numeric value (coerced to string) round-trips + assert.equal(await client.arSet('key', 10, (12345).toString()), 1); + assert.equal(await client.arGet('key', 10), '12345'); + + // empty string + assert.equal(await client.arSet('key', 11, ''), 1); + assert.equal(await client.arGet('key', 11), ''); + + // random bytes survive round-trip when read back as Buffer + const bytes = randomBytes(64); + assert.equal(await client.arSet('key', 12, bytes), 1); + assert.deepEqual( + await client.withTypeMapping({ [RESP_TYPES.BLOB_STRING]: Buffer }).arGet('key', 12), + bytes + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8, 8], 'LATEST'], 'arSet with multiple values returns count of newly-filled slots', async client => { + // 3 brand-new slots at 0,1,2 -> 3 + assert.equal(await client.arSet('multi', 0, ['a', 'b', 'c']), 3); + // overwrite the same 3 slots -> 0 + assert.equal(await client.arSet('multi', 0, ['x', 'y', 'z']), 0); + // starts at 2 (filled), extends to 3,4 (new) -> 2 + assert.equal(await client.arSet('multi', 2, ['p', 'q', 'r']), 2); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/ARSET.ts b/packages/client/lib/commands/ARSET.ts new file mode 100644 index 0000000000..6127e1f3d9 --- /dev/null +++ b/packages/client/lib/commands/ARSET.ts @@ -0,0 +1,18 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + index: number | string, + value: RedisVariadicArgument + ) { + parser.push('ARSET'); + parser.pushKey(key); + parser.push(index.toString()); + parser.pushVariadic(value); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index cfa8949386..5aee0ef7cc 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -13,6 +13,25 @@ import ACL_SETUSER from './ACL_SETUSER'; import ACL_USERS from './ACL_USERS'; import ACL_WHOAMI from './ACL_WHOAMI'; import APPEND from './APPEND'; +import ARCOUNT from './ARCOUNT'; +import ARDEL from './ARDEL'; +import ARDELRANGE from './ARDELRANGE'; +import ARGET from './ARGET'; +import ARGETRANGE from './ARGETRANGE'; +import ARGREP, { AR_PREDICATE_TYPES, AR_PREDICATE_COMBINATORS } from './ARGREP'; +import ARGREP_WITHVALUES from './ARGREP_WITHVALUES'; +import ARINFO from './ARINFO'; +import ARINSERT from './ARINSERT'; +import ARLASTITEMS from './ARLASTITEMS'; +import ARLEN from './ARLEN'; +import ARMGET from './ARMGET'; +import ARMSET from './ARMSET'; +import ARNEXT from './ARNEXT'; +import AROP, { AR_OPERATIONS } from './AROP'; +import ARRING from './ARRING'; +import ARSCAN from './ARSCAN'; +import ARSEEK from './ARSEEK'; +import ARSET from './ARSET'; import ASKING from './ASKING'; import AUTH from './AUTH'; import BGREWRITEAOF from './BGREWRITEAOF'; @@ -375,6 +394,9 @@ import VSIM_WITHSCORES from './VSIM_WITHSCORES'; import LATENCY_HISTOGRAM from './LATENCY_HISTOGRAM'; export { + AR_PREDICATE_TYPES, + AR_PREDICATE_COMBINATORS, + AR_OPERATIONS, CLIENT_KILL_FILTERS, CLIENT_UNBLOCK_MODES, FAILOVER_MODES, @@ -520,6 +542,276 @@ export default { * @param value - The value to append */ append: APPEND, + /** + * Returns the number of non-empty elements in the array stored at the given key + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARCOUNT, + /** + * Returns the number of non-empty elements in the array stored at the given key + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arCount: ARCOUNT, + /** + * Deletes elements at the specified indices in the array stored at the given key + * @param key - Key of the array + * @param indices - Index or indices to delete + */ + ARDEL, + /** + * Deletes elements at the specified indices in the array stored at the given key + * @param key - Key of the array + * @param indices - Index or indices to delete + */ + arDel: ARDEL, + /** + * Deletes elements within one or more inclusive index ranges in the array stored at the given key + * @param key - Key of the array + * @param ranges - A `[start, end]` range or an array of `[start, end]` ranges + */ + ARDELRANGE, + /** + * Deletes elements within one or more inclusive index ranges in the array stored at the given key + * @param key - Key of the array + * @param ranges - A `[start, end]` range or an array of `[start, end]` ranges + */ + arDelRange: ARDELRANGE, + /** + * Returns the value at the given index in the array stored at the given key + * @param key - Key of the array + * @param index - Index to read + */ + ARGET, + /** + * Returns the value at the given index in the array stored at the given key + * @param key - Key of the array + * @param index - Index to read + */ + arGet: ARGET, + /** + * Returns the values in the inclusive index range [start, end] in the array stored at the given key + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + */ + ARGETRANGE, + /** + * Returns the values in the inclusive index range [start, end] in the array stored at the given key + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + */ + arGetRange: ARGETRANGE, + /** + * Searches elements of the array stored at the given key using one or more textual predicates and returns matching indices + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param predicates - Array of `[type, value]` predicates + * @param options - Optional COMBINATOR, LIMIT and NOCASE modifiers + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARGREP, + /** + * Searches elements of the array stored at the given key using one or more textual predicates and returns matching indices + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param predicates - Array of `[type, value]` predicates + * @param options - Optional COMBINATOR, LIMIT and NOCASE modifiers + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arGrep: ARGREP, + /** + * Searches elements of the array stored at the given key using one or more textual predicates and returns matching `{index, value}` pairs + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param predicates - Array of `[type, value]` predicates + * @param options - Optional COMBINATOR, LIMIT and NOCASE modifiers + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARGREP_WITHVALUES, + /** + * Searches elements of the array stored at the given key using one or more textual predicates and returns matching `{index, value}` pairs + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param predicates - Array of `[type, value]` predicates + * @param options - Optional COMBINATOR, LIMIT and NOCASE modifiers + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arGrepWithValues: ARGREP_WITHVALUES, + /** + * Returns metadata about the array stored at the given key + * @param key - Key of the array + * @param options - Optional FULL flag for per-slice statistics + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARINFO, + /** + * Returns metadata about the array stored at the given key + * @param key - Key of the array + * @param options - Optional FULL flag for per-slice statistics + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arInfo: ARINFO, + /** + * Inserts values at consecutive indices in the array stored at the given key, beginning at the current insert cursor position + * @param key - Key of the array + * @param values - Value or values to insert + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARINSERT, + /** + * Inserts values at consecutive indices in the array stored at the given key, beginning at the current insert cursor position + * @param key - Key of the array + * @param values - Value or values to insert + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arInsert: ARINSERT, + /** + * Returns up to `count` most recently inserted elements from the array stored at the given key + * @param key - Key of the array + * @param count - Maximum number of items to return + * @param options - Optional REV flag for reverse chronological order + */ + ARLASTITEMS, + /** + * Returns up to `count` most recently inserted elements from the array stored at the given key + * @param key - Key of the array + * @param count - Maximum number of items to return + * @param options - Optional REV flag for reverse chronological order + */ + arLastItems: ARLASTITEMS, + /** + * Returns the length of the array stored at the given key (max set index + 1) + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARLEN, + /** + * Returns the length of the array stored at the given key (max set index + 1) + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arLen: ARLEN, + /** + * Returns the values at the specified indices in the array stored at the given key + * @param key - Key of the array + * @param indices - Index or indices to read + */ + ARMGET, + /** + * Returns the values at the specified indices in the array stored at the given key + * @param key - Key of the array + * @param indices - Index or indices to read + */ + arMGet: ARMGET, + /** + * Sets multiple index/value pairs in the array stored at the given key + * @param key - Key of the array + * @param entries - An object, Map, or array of `[index, value]` tuples + */ + ARMSET, + /** + * Sets multiple index/value pairs in the array stored at the given key + * @param key - Key of the array + * @param entries - An object, Map, or array of `[index, value]` tuples + */ + arMSet: ARMSET, + /** + * Returns the next index ARINSERT would use for the array stored at the given key + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARNEXT, + /** + * Returns the next index ARINSERT would use for the array stored at the given key + * @param key - Key of the array + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arNext: ARNEXT, + /** + * Performs an aggregate operation on elements of the array stored at the given key in an inclusive index range + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param operation - SUM, MIN, MAX, AND, OR, XOR, MATCH or USED + * @param value - Required value when operation is MATCH + */ + AROP, + /** + * Performs an aggregate operation on elements of the array stored at the given key in an inclusive index range + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param operation - SUM, MIN, MAX, AND, OR, XOR, MATCH or USED + * @param value - Required value when operation is MATCH + */ + arOp: AROP, + /** + * Inserts values into the array stored at the given key as a fixed-size ring buffer + * @param key - Key of the array + * @param size - Number of slots in the ring buffer + * @param values - Value or values to insert + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARRING, + /** + * Inserts values into the array stored at the given key as a fixed-size ring buffer + * @param key - Key of the array + * @param size - Number of slots in the ring buffer + * @param values - Value or values to insert + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arRing: ARRING, + /** + * Iterates populated elements of the array stored at the given key in the inclusive index range and returns alternating index/value pairs + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param options - Optional LIMIT modifier + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + ARSCAN, + /** + * Iterates populated elements of the array stored at the given key in the inclusive index range and returns alternating index/value pairs + * @param key - Key of the array + * @param start - Start index (inclusive) + * @param end - End index (inclusive) + * @param options - Optional LIMIT modifier + * @remarks Returned indices may exceed `Number.MAX_SAFE_INTEGER` (2^53-1). For full precision, use `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. + */ + arScan: ARSCAN, + /** + * Sets the insert cursor of the array stored at the given key to `index` + * @param key - Key of the array + * @param index - New cursor position + */ + ARSEEK, + /** + * Sets the insert cursor of the array stored at the given key to `index` + * @param key - Key of the array + * @param index - New cursor position + */ + arSeek: ARSEEK, + /** + * Sets one or more contiguous values in the array stored at the given key starting at `index` + * @param key - Key of the array + * @param index - Starting index + * @param value - A single value or an array of values stored at consecutive indices + */ + ARSET, + /** + * Sets one or more contiguous values in the array stored at the given key starting at `index` + * @param key - Key of the array + * @param index - Starting index + * @param value - A single value or an array of values stored at consecutive indices + */ + arSet: ARSET, /** * Tells a Redis cluster node that the client is ok receiving such redirects */