1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-09 00:22:08 +03:00

add support for List commands, fix some Sorted Set commands, add some cluster commands, spawn cluster for testing, add support for command options in cluster, and more

This commit is contained in:
leibale
2021-06-23 18:12:12 -04:00
parent ff6125c423
commit faab94fab2
52 changed files with 1096 additions and 54 deletions

View File

@@ -193,14 +193,14 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
} }
} }
async executeScript<S extends RedisLuaScript>(script: S, args: Array<string>): Promise<ReturnType<S['transformReply']>> { async executeScript<S extends RedisLuaScript>(script: S, args: Array<string>, options?: ClientCommandOptions): Promise<ReturnType<S['transformReply']>> {
try { try {
return await this.sendCommand([ return await this.sendCommand([
'EVALSHA', 'EVALSHA',
script.SHA, script.SHA,
script.NUMBER_OF_KEYS.toString(), script.NUMBER_OF_KEYS.toString(),
...args ...args
]); ], options);
} catch (err: any) { } catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) { if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err; throw err;
@@ -211,7 +211,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
script.SCRIPT, script.SCRIPT,
script.NUMBER_OF_KEYS.toString(), script.NUMBER_OF_KEYS.toString(),
...args ...args
]); ], options);
} }
} }

View File

@@ -1,19 +1,21 @@
import calculateSlot from 'cluster-key-slot'; import calculateSlot from 'cluster-key-slot';
import RedisClient from './client'; import RedisClient, { RedisClientType } from './client';
import { RedisSocketOptions } from './socket'; import { RedisSocketOptions } from './socket';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from './commands/CLUSTER_NODES'; import { RedisClusterMasterNode, RedisClusterReplicaNode } from './commands/CLUSTER_NODES';
import { RedisClusterOptions } from './cluster'; import { RedisClusterOptions } from './cluster';
import { RedisModules } from './commands';
import { RedisLuaScripts } from './lua-script';
interface SlotClients { interface SlotClients<M extends RedisModules, S extends RedisLuaScripts> {
master: RedisClient; master: RedisClientType<M, S>;
replicas: Array<RedisClient>; replicas: Array<RedisClientType<M, S>>;
iterator: IterableIterator<RedisClient> | undefined; iterator: IterableIterator<RedisClientType<M, S>> | undefined;
} }
export default class RedisClusterSlots { export default class RedisClusterSlots<M extends RedisModules, S extends RedisLuaScripts> {
readonly #options: RedisClusterOptions; readonly #options: RedisClusterOptions;
readonly #clientByKey = new Map<string, RedisClient>(); readonly #clientByKey = new Map<string, RedisClientType<M, S>>();
readonly #slots: Array<SlotClients> = []; readonly #slots: Array<SlotClients<M, S>> = [];
constructor(options: RedisClusterOptions) { constructor(options: RedisClusterOptions) {
this.#options = options; this.#options = options;
@@ -32,7 +34,7 @@ export default class RedisClusterSlots {
throw new Error('None of the root nodes is available'); throw new Error('None of the root nodes is available');
} }
async discover(startWith: RedisClient): Promise<void> { async discover(startWith: RedisClientType<M, S>): Promise<void> {
try { try {
await this.#discoverNodes(startWith.options?.socket); await this.#discoverNodes(startWith.options?.socket);
return; return;
@@ -99,7 +101,7 @@ export default class RedisClusterSlots {
} }
} }
#initiateClientForNode(node: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): RedisClient { #initiateClientForNode(node: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): RedisClientType<M, S> {
clientsInUse.add(node.url); clientsInUse.add(node.url);
let client = this.#clientByKey.get(node.url); let client = this.#clientByKey.get(node.url);
@@ -118,11 +120,11 @@ export default class RedisClusterSlots {
return client; return client;
} }
#getSlotMaster(slot: number): RedisClient { #getSlotMaster(slot: number): RedisClientType<M, S> {
return this.#slots[slot].master; return this.#slots[slot].master;
} }
*#slotIterator(slotNumber: number): IterableIterator<RedisClient> { *#slotIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
const slot = this.#slots[slotNumber]; const slot = this.#slots[slotNumber];
yield slot.master; yield slot.master;
@@ -131,7 +133,7 @@ export default class RedisClusterSlots {
} }
} }
#getSlotClient(slotNumber: number): RedisClient { #getSlotClient(slotNumber: number): RedisClientType<M, S> {
const slot = this.#slots[slotNumber]; const slot = this.#slots[slotNumber];
if (!slot.iterator) { if (!slot.iterator) {
slot.iterator = this.#slotIterator(slotNumber); slot.iterator = this.#slotIterator(slotNumber);
@@ -146,9 +148,9 @@ export default class RedisClusterSlots {
return value; return value;
} }
#randomClientIterator?: IterableIterator<RedisClient>; #randomClientIterator?: IterableIterator<RedisClientType<M, S>>;
#getRandomClient(): RedisClient { #getRandomClient(): RedisClientType<M, S> {
if (!this.#clientByKey.size) { if (!this.#clientByKey.size) {
throw new Error('Cluster is not connected'); throw new Error('Cluster is not connected');
} }
@@ -166,7 +168,7 @@ export default class RedisClusterSlots {
return value; return value;
} }
getClient(firstKey?: string, isReadonly?: boolean): RedisClient { getClient(firstKey?: string, isReadonly?: boolean): RedisClientType<M, S> {
if (!firstKey) { if (!firstKey) {
return this.#getRandomClient(); return this.#getRandomClient();
} }
@@ -179,6 +181,18 @@ export default class RedisClusterSlots {
return this.#getSlotClient(slot); return this.#getSlotClient(slot);
} }
getMasters(): Array<RedisClientType<M, S>> {
const masters = [];
for (const client of this.#clientByKey.values()) {
if (client.options?.readonly) continue;
masters.push(client);
}
return masters;
}
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
await Promise.all( await Promise.all(
[...this.#clientByKey.values()].map(client => client.disconnect()) [...this.#clientByKey.values()].map(client => client.disconnect())

View File

@@ -1,11 +1,10 @@
import COMMANDS from './commands'; import COMMANDS from './commands';
import { RedisCommand, RedisModules } from './commands'; import { RedisCommand, RedisModules } from './commands';
import RedisClient, { WithPlugins } from './client'; import { ClientCommandOptions, RedisClientType, WithPlugins } from './client';
import { RedisSocketOptions } from './socket'; import { RedisSocketOptions } from './socket';
import RedisClusterSlots from './cluster-slots'; import RedisClusterSlots from './cluster-slots';
import { RedisLuaScript, RedisLuaScripts } from './lua-script'; import { RedisLuaScript, RedisLuaScripts } from './lua-script';
import { QueueCommandOptions } from './commands-queue'; import { commandOptions, CommandOptions, isCommandOptions } from './command-options';
import { CommandOptions, isCommandOptions } from './command-options';
export interface RedisClusterOptions<M = RedisModules, S = RedisLuaScripts> { export interface RedisClusterOptions<M = RedisModules, S = RedisLuaScripts> {
rootNodes: Array<RedisSocketOptions>; rootNodes: Array<RedisSocketOptions>;
@@ -26,6 +25,7 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
await this.sendCommand( await this.sendCommand(
command, command,
command.transformArguments(...(options ? args.slice(1) : args)), command.transformArguments(...(options ? args.slice(1) : args)),
options
) )
); );
}; };
@@ -35,12 +35,12 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
return <any>new RedisCluster(options); return <any>new RedisCluster(options);
} }
static commandOptions(options: QueueCommandOptions): CommandOptions<QueueCommandOptions> { static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return this.commandOptions(options); return commandOptions(options);
} }
readonly #options: RedisClusterOptions; readonly #options: RedisClusterOptions;
readonly #slots: RedisClusterSlots; readonly #slots: RedisClusterSlots<M, S>;
constructor(options: RedisClusterOptions<M, S>) { constructor(options: RedisClusterOptions<M, S>) {
this.#options = options; this.#options = options;
@@ -68,7 +68,8 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
return script.transformReply( return script.transformReply(
await this.executeScript( await this.executeScript(
script, script,
script.transformArguments(...(options ? args.slice(1) : args)) script.transformArguments(...(options ? args.slice(1) : args)),
options
) )
); );
}; };
@@ -79,42 +80,42 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
return this.#slots.connect(); return this.#slots.connect();
} }
async sendCommand<C extends RedisCommand>(command: C, args: Array<string>, redirections: number = 0): Promise<ReturnType<C['transformReply']>> { async sendCommand<C extends RedisCommand>(command: C, args: Array<string>, options?: ClientCommandOptions, redirections: number = 0): Promise<ReturnType<C['transformReply']>> {
const client = this.#getClient(command, args); const client = this.#getClient(command, args);
try { try {
return await client.sendCommand(args); return await client.sendCommand(args, options);
} catch (err) { } catch (err) {
if (await this.#handleCommandError(err, client, redirections)) { if (await this.#handleCommandError(err, client, redirections)) {
return this.sendCommand(command, args, redirections + 1); return this.sendCommand(command, args, options, redirections + 1);
} }
throw err; throw err;
} }
} }
async executeScript<S extends RedisLuaScript>(script: S, args: Array<string>, redirections: number = 0): Promise<ReturnType<S['transformReply']>> { async executeScript<S extends RedisLuaScript>(script: S, args: Array<string>, options?: ClientCommandOptions, redirections: number = 0): Promise<ReturnType<S['transformReply']>> {
const client = this.#getClient(script, args); const client = this.#getClient(script, args);
try { try {
return await client.executeScript(script, args); return await client.executeScript(script, args, options);
} catch (err) { } catch (err) {
if (await this.#handleCommandError(err, client, redirections)) { if (await this.#handleCommandError(err, client, redirections)) {
return this.executeScript(script, args, redirections + 1); return this.executeScript(script, args, options, redirections + 1);
} }
throw err; throw err;
} }
} }
#getClient(commandOrScript: RedisCommand | RedisLuaScript, args: Array<string>): RedisClient { #getClient(commandOrScript: RedisCommand | RedisLuaScript, args: Array<string>): RedisClientType<M, S> {
return this.#slots.getClient( return this.#slots.getClient(
commandOrScript.FIRST_KEY_INDEX ? args[commandOrScript.FIRST_KEY_INDEX] : undefined, commandOrScript.FIRST_KEY_INDEX ? args[commandOrScript.FIRST_KEY_INDEX] : undefined,
commandOrScript.IS_READ_ONLY commandOrScript.IS_READ_ONLY
); );
} }
async #handleCommandError(err: Error, client: RedisClient, redirections: number = 0): Promise<boolean> { async #handleCommandError(err: Error, client: RedisClientType<M, S>, redirections: number = 0): Promise<boolean> {
if (redirections < (this.#options.maxCommandRedirections ?? 16)) { if (redirections < (this.#options.maxCommandRedirections ?? 16)) {
throw err; throw err;
} }
@@ -128,6 +129,10 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
throw err; throw err;
} }
getMasters(): Array<RedisClientType<M, S>> {
return this.#slots.getMasters();
}
disconnect(): Promise<void> { disconnect(): Promise<void> {
return this.#slots.disconnect(); return this.#slots.disconnect();
} }

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './BLMOVE';
import RedisClient from '../client';
import RedisCluster from '../cluster';
describe('BLMOVE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
);
});
itWithClient(TestRedisServers.OPEN, 'client.blMove', async client => {
const [blMoveReply] = await Promise.all([
client.blMove(RedisClient.commandOptions({
duplicateConnection: true
}), 'source', 'destination', 'LEFT', 'RIGHT', 0),
client.lPush('source', 'element')
]);
assert.equal(
blMoveReply,
'element'
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.blMove', async cluster => {
const [blMoveReply] = await Promise.all([
cluster.blMove(RedisCluster.commandOptions({
duplicateConnection: true
}), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0),
cluster.lPush('{tag}source', 'element')
]);
assert.equal(
blMoveReply,
'element'
);
});
});

23
lib/commands/BLMOVE.ts Normal file
View File

@@ -0,0 +1,23 @@
import { transformReplyStringNull } from './generic-transformers';
import { LMoveSide } from './LMOVE';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(
source: string,
destination: string,
sourceDirection: LMoveSide,
destinationDirection: LMoveSide,
timeout: number
): Array<string> {
return [
'BLMOVE',
source,
destination,
sourceDirection,
destinationDirection,
timeout.toString()
];
}
export const transformReply = transformReplyStringNull;

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils'; import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './BLPOP'; import { transformArguments } from './BLPOP';
import RedisClient from '../client'; import RedisClient from '../client';
@@ -25,12 +25,32 @@ describe('BLPOP', () => {
client.blPop(RedisClient.commandOptions({ client.blPop(RedisClient.commandOptions({
duplicateConnection: true duplicateConnection: true
}), 'key', 0), }), 'key', 0),
client.lPush('key', ['1', '2']) client.lPush('key', 'element')
]); ]);
assert.deepEqual( assert.deepEqual(
popReply, popReply,
['key', '2'] {
key: 'key',
element: 'element'
}
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.blPop', async cluster => {
const [popReply] = await Promise.all([
cluster.blPop(RedisClient.commandOptions({
duplicateConnection: true
}), 'key', 0),
cluster.lPush('key', 'element')
]);
assert.deepEqual(
popReply,
{
key: 'key',
element: 'element'
}
); );
}); });
}); });

View File

@@ -1,4 +1,4 @@
export const FIRST_KEY_INDEX = 0; export const FIRST_KEY_INDEX = 1;
export function transformArguments(keys: string | Array<string>, timeout: number): Array<string> { export function transformArguments(keys: string | Array<string>, timeout: number): Array<string> {
const args = ['BLPOP']; const args = ['BLPOP'];
@@ -14,8 +14,16 @@ export function transformArguments(keys: string | Array<string>, timeout: number
return args; return args;
} }
type BLPOPReply = [list: string, value: string]; type BLPOPReply = null | {
key: string;
element: string;
};
export function transformReply(reply: BLPOPReply): BLPOPReply { export function transformReply(reply: null | [string, string]): BLPOPReply {
return reply; if (reply === null) return null;
return {
key: reply[0],
element: reply[1]
};
} }

View File

@@ -0,0 +1,57 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './BRPOP';
import RedisClient from '../client';
import RedisCluster from '../cluster';
describe('BRPOP', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments('key', 0),
['BRPOP', 'key', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments(['key1', 'key2'], 0),
['BRPOP', 'key1', 'key2', '0']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.brPop', async client => {
const [brPopReply] = await Promise.all([
client.brPop(RedisClient.commandOptions({
duplicateConnection: true
}), 'key', 0),
client.lPush('key', 'element')
]);
assert.deepEqual(
brPopReply,
{
key: 'key',
element: 'element'
}
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.brPop', async cluster => {
const [brPopReply] = await Promise.all([
cluster.brPop(RedisCluster.commandOptions({
duplicateConnection: true
}), 'key', 0),
cluster.lPush('key', 'element')
]);
assert.deepEqual(
brPopReply,
{
key: 'key',
element: 'element'
}
);
});
});

31
lib/commands/BRPOP.ts Normal file
View File

@@ -0,0 +1,31 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string | Array<string>, timeout: number): Array<string> {
const args = ['BRPOP'];
if (typeof key === 'string') {
args.push(key);
} else {
args.push(...key);
}
args.push(timeout.toString());
return args;
}
type BRPOPReply = null | {
key: string;
element: string;
};
export function transformReply(reply: null | [string, string]): BRPOPReply {
if (reply === null) return null;
return {
key: reply[0],
element: reply[1]
};
}

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import RedisClient from '../client';
import RedisCluster from '../cluster';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './BRPOPLPUSH';
describe('BRPOPLPUSH', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 0),
['BRPOPLPUSH', 'source', 'destination', '0']
);
});
itWithClient(TestRedisServers.OPEN, 'client.brPopLPush', async client => {
const [popReply] = await Promise.all([
client.brPopLPush(RedisClient.commandOptions({
duplicateConnection: true
}), 'source', 'destination', 0),
client.lPush('source', 'element')
]);
assert.equal(
popReply,
'element'
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.brPopLPush', async cluster => {
const [popReply] = await Promise.all([
cluster.brPopLPush(RedisCluster.commandOptions({
duplicateConnection: true
}), '{tag}source', '{tag}destination', 0),
cluster.lPush('{tag}source', 'element')
]);
assert.equal(
popReply,
'element'
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyNumber, transformReplyNumberNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(source: string, destination: string, timeout: number): Array<string> {
return ['BRPOPLPUSH', source, destination, timeout.toString()];
}
export const transformReply = transformReplyNumberNull;

View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_RESET';
describe('CLUSTER RESET', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'RESET']
);
});
it('simple', () => {
assert.deepEqual(
transformArguments('HARD'),
['CLUSTER', 'RESET', 'HARD']
);
});
it('simple', () => {
assert.deepEqual(
transformArguments('SOFT'),
['CLUSTER', 'RESET', 'SOFT']
);
});
});
});

View File

@@ -0,0 +1,15 @@
import { transformReplyString } from './generic-transformers';
export type ClusterResetModes = 'HARD' | 'SOFT';
export function transformArguments(mode?: ClusterResetModes): Array<string> {
const args = ['CLUSTER', 'RESET'];
if (mode) {
args.push(mode);
}
return args;
}
export const transformReply = transformReplyString;

View File

@@ -1,3 +1,4 @@
import { transformReplyStringArrayNull } from './generic-transformers';
import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD'; import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD';
export { FIRST_KEY_INDEX } from './HRANDFIELD'; export { FIRST_KEY_INDEX } from './HRANDFIELD';
@@ -9,6 +10,4 @@ export function transformArguments(key: string, count: number): Array<string> {
]; ];
} }
export function transformReply(reply: Array<string> | null): Array<string> | null { export const transformReply = transformReplyStringArrayNull;
return reply;
}

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LINDEX';
describe('LINDEX', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'element'),
['LINDEX', 'key', 'element']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lIndex', async client => {
assert.equal(
await client.lIndex('key', 'element'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lIndex', async cluster => {
assert.equal(
await cluster.lIndex('key', 'element'),
null
);
});
});

11
lib/commands/LINDEX.ts Normal file
View File

@@ -0,0 +1,11 @@
import { transformReplyNumberNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string, element: string): Array<string> {
return ['LINDEX', key, element];
}
export const transformReply = transformReplyNumberNull;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LINSERT';
describe('LINSERT', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'BEFORE', 'pivot', 'element'),
['LINSERT', 'key', 'BEFORE', 'pivot', 'element']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lInsert', async client => {
assert.equal(
await client.lInsert('key', 'BEFORE', 'pivot', 'element'),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lLen', async cluster => {
assert.equal(
await cluster.lInsert('key', 'BEFORE', 'pivot', 'element'),
0
);
});
});

22
lib/commands/LINSERT.ts Normal file
View File

@@ -0,0 +1,22 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
type LInsertPosition = 'BEFORE' | 'AFTER';
export function transformArguments(
key: string,
position: LInsertPosition,
pivot: string,
element: string
): Array<string> {
return [
'LINSERT',
key,
position,
pivot,
element
];
}
export const transformReply = transformReplyNumber;

26
lib/commands/LLEN.spec.ts Normal file
View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LLEN';
describe('LLEN', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['LLEN', 'key']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lLen', async client => {
assert.equal(
await client.lLen('key'),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lLen', async cluster => {
assert.equal(
await cluster.lLen('key'),
0
);
});
});

11
lib/commands/LLEN.ts Normal file
View File

@@ -0,0 +1,11 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string): Array<string> {
return ['LLEN', key];
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LMOVE';
describe('LMOVE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 'LEFT', 'RIGHT'),
['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lMove', async client => {
assert.equal(
await client.lMove('source', 'destination', 'LEFT', 'RIGHT'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lMove', async cluster => {
assert.equal(
await cluster.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'),
null
);
});
});

22
lib/commands/LMOVE.ts Normal file
View File

@@ -0,0 +1,22 @@
import { transformReplyStringNull } from './generic-transformers';
export type LMoveSide = 'LEFT' | 'RIGHT';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(
source: string,
destination: string,
sourceSide: LMoveSide,
destinationSide: LMoveSide
): Array<string> {
return [
'LMOVE',
source,
destination,
sourceSide,
destinationSide,
];
}
export const transformReply = transformReplyStringNull;

26
lib/commands/LPOP.spec.ts Normal file
View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LPOP';
describe('LPOP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['LPOP', 'key']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lPop', async client => {
assert.equal(
await client.lPop('key'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lPop', async cluster => {
assert.equal(
await cluster.lPop('key'),
null
);
});
});

9
lib/commands/LPOP.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyStringNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string): Array<string> {
return ['LPOP', key];
}
export const transformReply = transformReplyStringNull;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LPOP_COUNT';
describe('LPOP COUNT', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 1),
['LPOP', 'key', '1']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lPopCount', async client => {
assert.equal(
await client.lPopCount('key', 1),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lPop', async cluster => {
assert.equal(
await cluster.lPopCount('key', 1),
null
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyStringArrayNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, count: number): Array<string> {
return ['LPOP', key, count.toString()];
}
export const transformReply = transformReplyStringArrayNull;

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils'; import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LPUSH'; import { transformArguments } from './LPUSH';
describe('LPUSH', () => { describe('LPUSH', () => {
@@ -25,4 +25,11 @@ describe('LPUSH', () => {
1 1
); );
}); });
itWithCluster(TestRedisClusters.OPEN, 'cluster.lPush', async cluster => {
assert.equal(
await cluster.lPush('key', 'field'),
1
);
});
}); });

View File

@@ -1,6 +1,6 @@
import { transformReplyNumber } from './generic-transformers'; import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 0; export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, elements: string | Array<string>): Array<string> { export function transformArguments(key: string, elements: string | Array<string>): Array<string> {
const args = [ const args = [

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LPUSHX';
describe('LPUSHX', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key', 'element'),
['LPUSHX', 'key', 'element']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['LPUSHX', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.lPushX', async client => {
assert.equal(
await client.lPushX('key', 'element'),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lPushX', async cluster => {
assert.equal(
await cluster.lPushX('key', 'element'),
0
);
});
});

20
lib/commands/LPUSHX.ts Normal file
View File

@@ -0,0 +1,20 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, element: string | Array<string>): Array<string> {
const args = [
'LPUSHX',
key
];
if (typeof element === 'string') {
args.push(element);
} else {
args.push(...element);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LRANGE';
describe('LRANGE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 0, -1),
['LRANGE', 'key', '0', '-1']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lRange', async client => {
assert.deepEqual(
await client.lRange('key', 0, -1),
[]
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lRange', async cluster => {
assert.deepEqual(
await cluster.lRange('key', 0, -1),
[]
);
});
});

16
lib/commands/LRANGE.ts Normal file
View File

@@ -0,0 +1,16 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string, start: number, stop: number): Array<string> {
return [
'LRANGE',
key,
start.toString(),
stop.toString()
];
}
export const transformReply = transformReplyStringArray;

27
lib/commands/LREM.spec.ts Normal file
View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LREM';
describe('LREM', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 0, 'element'),
['LREM', 'key', '0', 'element']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lRem', async client => {
assert.equal(
await client.lRem('key', 0, 'element'),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lRem', async cluster => {
assert.equal(
await cluster.lRem('key', 0, 'element'),
0
);
});
});

14
lib/commands/LREM.ts Normal file
View File

@@ -0,0 +1,14 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, count: number, element: string): Array<string> {
return [
'LREM',
key,
count.toString(),
element
];
}
export const transformReply = transformReplyNumber;

28
lib/commands/LSET.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LSET';
describe('LSET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 0, 'element'),
['LSET', 'key', '0', 'element']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lSet', async client => {
await client.lPush('key', 'element');
assert.equal(
await client.lSet('key', 0, 'element'),
'OK'
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lSet', async cluster => {
await cluster.lPush('key', 'element');
assert.equal(
await cluster.lSet('key', 0, 'element'),
'OK'
);
});
});

14
lib/commands/LSET.ts Normal file
View File

@@ -0,0 +1,14 @@
import { transformReplyString } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, index: number, element: string): Array<string> {
return [
'LSET',
key,
index.toString(),
element
];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './LTRIM';
describe('LTRIM', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 0, -1),
['LTRIM', 'key', '0', '-1']
);
});
itWithClient(TestRedisServers.OPEN, 'client.lTrim', async client => {
assert.equal(
await client.lTrim('key', 0, -1),
'OK'
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.lTrim', async cluster => {
assert.equal(
await cluster.lTrim('key', 0, -1),
'OK'
);
});
});

14
lib/commands/LTRIM.ts Normal file
View File

@@ -0,0 +1,14 @@
import { transformReplyString } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, start: number, stop: number): Array<string> {
return [
'LTRIM',
key,
start.toString(),
stop.toString()
]
}
export const transformReply = transformReplyString;

26
lib/commands/RPOP.spec.ts Normal file
View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './RPOP';
describe('RPOP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['RPOP', 'key']
);
});
itWithClient(TestRedisServers.OPEN, 'client.rPop', async client => {
assert.equal(
await client.rPop('key'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.rPop', async cluster => {
assert.equal(
await cluster.rPop('key'),
null
);
});
});

9
lib/commands/RPOP.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyStringNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string): Array<string> {
return ['RPOP', key];
}
export const transformReply = transformReplyStringNull;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './RPOPLPUSH';
describe('RPOPLPUSH', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination'),
['RPOPLPUSH', 'source', 'destination']
);
});
itWithClient(TestRedisServers.OPEN, 'client.rPopLPush', async client => {
assert.equal(
await client.rPopLPush('source', 'destination'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.rPopLPush', async cluster => {
assert.equal(
await cluster.rPopLPush('{tag}source', '{tag}destination'),
null
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyNumberNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(source: string, destination: string): Array<string> {
return ['RPOPLPUSH', source, destination];
}
export const transformReply = transformReplyNumberNull;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './RPOP_COUNT';
describe('RPOP COUNT', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 1),
['RPOP', 'key', '1']
);
});
itWithClient(TestRedisServers.OPEN, 'client.rPopCount', async client => {
assert.equal(
await client.rPopCount('key', 1),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.rPopCount', async cluster => {
assert.equal(
await cluster.rPopCount('key', 1),
null
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyStringArrayNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, count: number): Array<string> {
return ['RPOP', key, count.toString()];
}
export const transformReply = transformReplyStringArrayNull;

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './RPUSH';
describe('RPUSH', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key', 'element'),
['RPUSH', 'key', 'element']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['RPUSH', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.rPush', async client => {
assert.equal(
await client.rPush('key', 'element'),
1
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.rPush', async cluster => {
assert.equal(
await cluster.rPush('key', 'element'),
1
);
});
});

20
lib/commands/RPUSH.ts Normal file
View File

@@ -0,0 +1,20 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, element: string | Array<string>): Array<string> {
const args = [
'RPUSH',
key
];
if (typeof element === 'string') {
args.push(element);
} else {
args.push(...element);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils';
import { transformArguments } from './RPUSHX';
describe('RPUSHX', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key', 'element'),
['RPUSHX', 'key', 'element']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['RPUSHX', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.rPushX', async client => {
assert.equal(
await client.rPushX('key', 'element'),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.rPushX', async cluster => {
assert.equal(
await cluster.rPushX('key', 'element'),
0
);
});
});

20
lib/commands/RPUSHX.ts Normal file
View File

@@ -0,0 +1,20 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, element: string | Array<string>): Array<string> {
const args = [
'RPUSHX',
key
];
if (typeof element === 'string') {
args.push(element);
} else {
args.push(...element);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -1,3 +1,4 @@
import { transformReplyStringArrayNull } from './generic-transformers';
import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER'; import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER';
export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER'; export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER';
@@ -9,6 +10,4 @@ export function transformArguments(key: string, count: number): Array<string> {
]; ];
} }
export function transformReply(reply: Array<string> | null): Array<string> | null { export const transformReply = transformReplyStringArrayNull;
return reply;
}

View File

@@ -1,4 +1,4 @@
import { transformReplyStringArray } from './generic-transformers'; import { transformArgumentNumberInfinity, transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
@@ -13,12 +13,12 @@ interface ZRangeOptions {
}; };
} }
export function transformArguments(src: string, min: string | number, max: string | number, options?: ZRangeOptions): Array<string> { export function transformArguments(key: string, min: string | number, max: string | number, options?: ZRangeOptions): Array<string> {
const args = [ const args = [
'ZRANGE', 'ZRANGE',
src, key,
typeof min === 'string' ? min : min.toString(), typeof min === 'string' ? min : transformArgumentNumberInfinity(min),
typeof max === 'string' ? max : max.toString() typeof max === 'string' ? max : transformArgumentNumberInfinity(max)
]; ];
switch (options?.BY) { switch (options?.BY) {

View File

@@ -2,6 +2,10 @@ export function transformReplyNumber(reply: number): number {
return reply; return reply;
} }
export function transformReplyNumberNull(reply: number | null): number | null {
return reply;
}
export function transformReplyString(reply: string): string { export function transformReplyString(reply: string): string {
return reply; return reply;
} }
@@ -14,6 +18,10 @@ export function transformReplyStringArray(reply: Array<string>): Array<string> {
return reply; return reply;
} }
export function transformReplyStringArrayNull(reply: Array<string> | null): Array<string> | null {
return reply;
}
export function transformReplyBoolean(reply: number): boolean { export function transformReplyBoolean(reply: number): boolean {
return reply === 1; return reply === 1;
} }

View File

@@ -2,7 +2,10 @@ import * as APPEND from './APPEND';
import * as AUTH from './AUTH'; import * as AUTH from './AUTH';
import * as BITCOUNT from './BITCOUNT'; import * as BITCOUNT from './BITCOUNT';
import * as BITFIELD from './BITFIELD'; import * as BITFIELD from './BITFIELD';
import * as BLMOVE from './BLMOVE';
import * as BLPOP from './BLPOP'; import * as BLPOP from './BLPOP';
import * as BRPOP from './BRPOP';
import * as BRPOPLPUSH from './BRPOPLPUSH';
import * as BZPOPMAX from './BZPOPMAX'; import * as BZPOPMAX from './BZPOPMAX';
import * as BZPOPMIN from './BZPOPMIN'; import * as BZPOPMIN from './BZPOPMIN';
import * as CLIENT_INFO from './CLIENT_INFO'; import * as CLIENT_INFO from './CLIENT_INFO';
@@ -11,6 +14,7 @@ import * as CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS';
import * as CLUSTER_INFO from './CLUSTER_INFO'; import * as CLUSTER_INFO from './CLUSTER_INFO';
import * as CLUSTER_NODES from './CLUSTER_NODES'; import * as CLUSTER_NODES from './CLUSTER_NODES';
import * as CLUSTER_MEET from './CLUSTER_MEET'; import * as CLUSTER_MEET from './CLUSTER_MEET';
import * as CLUSTER_RESET from './CLUSTER_RESET';
import * as COPY from './COPY'; import * as COPY from './COPY';
import * as DECR from './DECR'; import * as DECR from './DECR';
import * as DECRBY from './DECRBY'; import * as DECRBY from './DECRBY';
@@ -41,7 +45,18 @@ import * as INCR from './INCR';
import * as INCRBY from './INCRBY'; import * as INCRBY from './INCRBY';
import * as INCRBYFLOAT from './INCRBYFLOAT'; import * as INCRBYFLOAT from './INCRBYFLOAT';
import * as KEYS from './KEYS'; import * as KEYS from './KEYS';
import * as LINDEX from './LINDEX';
import * as LINSERT from './LINSERT';
import * as LLEN from './LLEN';
import * as LMOVE from './LMOVE';
import * as LPOP from './LPOP';
import * as LPOP_COUNT from './LPOP_COUNT';
import * as LPUSH from './LPUSH'; import * as LPUSH from './LPUSH';
import * as LPUSHX from './LPUSHX';
import * as LRANGE from './LRANGE';
import * as LREM from './LREM';
import * as LSET from './LSET';
import * as LTRIM from './LTRIM';
import * as MOVE from './MOVE'; import * as MOVE from './MOVE';
import * as PERSIST from './PERSIST'; import * as PERSIST from './PERSIST';
import * as PEXPIRE from './PEXPIRE'; import * as PEXPIRE from './PEXPIRE';
@@ -56,6 +71,11 @@ import * as RANDOMKEY from './RANDOMKEY';
import * as READONLY from './READONLY'; import * as READONLY from './READONLY';
import * as RENAME from './RENAME'; import * as RENAME from './RENAME';
import * as RENAMENX from './RENAMENX'; import * as RENAMENX from './RENAMENX';
import * as RPOP_COUNT from './RPOP_COUNT';
import * as RPOP from './RPOP';
import * as RPOPLPUSH from './RPOPLPUSH';
import * as RPUSH from './RPUSH';
import * as RPUSHX from './RPUSHX';
import * as SADD from './SADD'; import * as SADD from './SADD';
import * as SCAN from './SCAN'; import * as SCAN from './SCAN';
import * as SCARD from './SCARD'; import * as SCARD from './SCARD';
@@ -145,8 +165,14 @@ export default {
bitCount: BITCOUNT, bitCount: BITCOUNT,
BITFIELD, BITFIELD,
bitField: BITFIELD, bitField: BITFIELD,
BLMOVE,
blMove: BLMOVE,
BLPOP, BLPOP,
blPop: BLPOP, blPop: BLPOP,
BRPOP,
brPop: BRPOP,
BRPOPLPUSH,
brPopLPush: BRPOPLPUSH,
BZPOPMAX, BZPOPMAX,
bzPopMax: BZPOPMAX, bzPopMax: BZPOPMAX,
BZPOPMIN, BZPOPMIN,
@@ -163,6 +189,8 @@ export default {
clusterNodes: CLUSTER_NODES, clusterNodes: CLUSTER_NODES,
CLUSTER_MEET, CLUSTER_MEET,
clusterMeet: CLUSTER_MEET, clusterMeet: CLUSTER_MEET,
CLUSTER_RESET,
clusterReset: CLUSTER_RESET,
COPY, COPY,
copy: COPY, copy: COPY,
DECR, DECR,
@@ -223,8 +251,30 @@ export default {
incrByFloat: INCRBYFLOAT, incrByFloat: INCRBYFLOAT,
KEYS, KEYS,
keys: KEYS, keys: KEYS,
LINDEX,
lIndex: LINDEX,
LINSERT,
lInsert: LINSERT,
LLEN,
lLen: LLEN,
LMOVE,
lMove: LMOVE,
LPOP_COUNT,
lPopCount: LPOP_COUNT,
LPOP,
lPop: LPOP,
LPUSH, LPUSH,
lPush: LPUSH, lPush: LPUSH,
LPUSHX,
lPushX: LPUSHX,
LRANGE,
lRange: LRANGE,
LREM,
lRem: LREM,
LSET,
lSet: LSET,
LTRIM,
lTrim: LTRIM,
MOVE, MOVE,
move: MOVE, move: MOVE,
PERSIST, PERSIST,
@@ -253,6 +303,16 @@ export default {
rename: RENAME, rename: RENAME,
RENAMENX, RENAMENX,
renameNX: RENAMENX, renameNX: RENAMENX,
RPOP_COUNT,
rPopCount: RPOP_COUNT,
RPOP,
rPop: RPOP,
RPOPLPUSH,
rPopLPush: RPOPLPUSH,
RPUSH,
rPush: RPUSH,
RPUSHX,
rPushX: RPUSHX,
SADD, SADD,
sAdd: SADD, sAdd: SADD,
SCAN, SCAN,