You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
V5 bringing RESP3, Sentinel and TypeMapping to node-redis
RESP3 Support - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion. Sentinel TypeMapping Correctly types Multi commands Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
@@ -1,473 +1,399 @@
|
||||
import * as ADD from './ADD';
|
||||
import * as ALTER from './ALTER';
|
||||
import * as CREATE from './CREATE';
|
||||
import * as CREATERULE from './CREATERULE';
|
||||
import * as DECRBY from './DECRBY';
|
||||
import * as DEL from './DEL';
|
||||
import * as DELETERULE from './DELETERULE';
|
||||
import * as GET from './GET';
|
||||
import * as INCRBY from './INCRBY';
|
||||
import * as INFO_DEBUG from './INFO_DEBUG';
|
||||
import * as INFO from './INFO';
|
||||
import * as MADD from './MADD';
|
||||
import * as MGET from './MGET';
|
||||
import * as MGET_WITHLABELS from './MGET_WITHLABELS';
|
||||
import * as QUERYINDEX from './QUERYINDEX';
|
||||
import * as RANGE from './RANGE';
|
||||
import * as REVRANGE from './REVRANGE';
|
||||
import * as MRANGE from './MRANGE';
|
||||
import * as MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
|
||||
import * as MREVRANGE from './MREVRANGE';
|
||||
import * as MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
|
||||
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
|
||||
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||
import type { DoubleReply, NumberReply, RedisArgument, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/lib/RESP/types';
|
||||
import ADD, { TsIgnoreOptions } from './ADD';
|
||||
import ALTER from './ALTER';
|
||||
import CREATE from './CREATE';
|
||||
import CREATERULE from './CREATERULE';
|
||||
import DECRBY from './DECRBY';
|
||||
import DEL from './DEL';
|
||||
import DELETERULE from './DELETERULE';
|
||||
import GET from './GET';
|
||||
import INCRBY from './INCRBY';
|
||||
import INFO_DEBUG from './INFO_DEBUG';
|
||||
import INFO from './INFO';
|
||||
import MADD from './MADD';
|
||||
import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS';
|
||||
import MGET_WITHLABELS from './MGET_WITHLABELS';
|
||||
import MGET from './MGET';
|
||||
import MRANGE_GROUPBY from './MRANGE_GROUPBY';
|
||||
import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY';
|
||||
import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS';
|
||||
import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY';
|
||||
import MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
|
||||
import MRANGE from './MRANGE';
|
||||
import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY';
|
||||
import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY';
|
||||
import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS';
|
||||
import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY';
|
||||
import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
|
||||
import MREVRANGE from './MREVRANGE';
|
||||
import QUERYINDEX from './QUERYINDEX';
|
||||
import RANGE from './RANGE';
|
||||
import REVRANGE from './REVRANGE';
|
||||
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/lib/commands/generic-transformers';
|
||||
import { RESP_TYPES } from '@redis/client/lib/RESP/decoder';
|
||||
|
||||
export default {
|
||||
ADD,
|
||||
add: ADD,
|
||||
ALTER,
|
||||
alter: ALTER,
|
||||
CREATE,
|
||||
create: CREATE,
|
||||
CREATERULE,
|
||||
createRule: CREATERULE,
|
||||
DECRBY,
|
||||
decrBy: DECRBY,
|
||||
DEL,
|
||||
del: DEL,
|
||||
DELETERULE,
|
||||
deleteRule: DELETERULE,
|
||||
GET,
|
||||
get: GET,
|
||||
INCRBY,
|
||||
incrBy: INCRBY,
|
||||
INFO_DEBUG,
|
||||
infoDebug: INFO_DEBUG,
|
||||
INFO,
|
||||
info: INFO,
|
||||
MADD,
|
||||
mAdd: MADD,
|
||||
MGET,
|
||||
mGet: MGET,
|
||||
MGET_WITHLABELS,
|
||||
mGetWithLabels: MGET_WITHLABELS,
|
||||
QUERYINDEX,
|
||||
queryIndex: QUERYINDEX,
|
||||
RANGE,
|
||||
range: RANGE,
|
||||
REVRANGE,
|
||||
revRange: REVRANGE,
|
||||
MRANGE,
|
||||
mRange: MRANGE,
|
||||
MRANGE_WITHLABELS,
|
||||
mRangeWithLabels: MRANGE_WITHLABELS,
|
||||
MREVRANGE,
|
||||
mRevRange: MREVRANGE,
|
||||
MREVRANGE_WITHLABELS,
|
||||
mRevRangeWithLabels: MREVRANGE_WITHLABELS
|
||||
};
|
||||
ADD,
|
||||
add: ADD,
|
||||
ALTER,
|
||||
alter: ALTER,
|
||||
CREATE,
|
||||
create: CREATE,
|
||||
CREATERULE,
|
||||
createRule: CREATERULE,
|
||||
DECRBY,
|
||||
decrBy: DECRBY,
|
||||
DEL,
|
||||
del: DEL,
|
||||
DELETERULE,
|
||||
deleteRule: DELETERULE,
|
||||
GET,
|
||||
get: GET,
|
||||
INCRBY,
|
||||
incrBy: INCRBY,
|
||||
INFO_DEBUG,
|
||||
infoDebug: INFO_DEBUG,
|
||||
INFO,
|
||||
info: INFO,
|
||||
MADD,
|
||||
mAdd: MADD,
|
||||
MGET_SELECTED_LABELS,
|
||||
mGetSelectedLabels: MGET_SELECTED_LABELS,
|
||||
MGET_WITHLABELS,
|
||||
mGetWithLabels: MGET_WITHLABELS,
|
||||
MGET,
|
||||
mGet: MGET,
|
||||
MRANGE_GROUPBY,
|
||||
mRangeGroupBy: MRANGE_GROUPBY,
|
||||
MRANGE_SELECTED_LABELS_GROUPBY,
|
||||
mRangeSelectedLabelsGroupBy: MRANGE_SELECTED_LABELS_GROUPBY,
|
||||
MRANGE_SELECTED_LABELS,
|
||||
mRangeSelectedLabels: MRANGE_SELECTED_LABELS,
|
||||
MRANGE_WITHLABELS_GROUPBY,
|
||||
mRangeWithLabelsGroupBy: MRANGE_WITHLABELS_GROUPBY,
|
||||
MRANGE_WITHLABELS,
|
||||
mRangeWithLabels: MRANGE_WITHLABELS,
|
||||
MRANGE,
|
||||
mRange: MRANGE,
|
||||
MREVRANGE_GROUPBY,
|
||||
mRevRangeGroupBy: MREVRANGE_GROUPBY,
|
||||
MREVRANGE_SELECTED_LABELS_GROUPBY,
|
||||
mRevRangeSelectedLabelsGroupBy: MREVRANGE_SELECTED_LABELS_GROUPBY,
|
||||
MREVRANGE_SELECTED_LABELS,
|
||||
mRevRangeSelectedLabels: MREVRANGE_SELECTED_LABELS,
|
||||
MREVRANGE_WITHLABELS_GROUPBY,
|
||||
mRevRangeWithLabelsGroupBy: MREVRANGE_WITHLABELS_GROUPBY,
|
||||
MREVRANGE_WITHLABELS,
|
||||
mRevRangeWithLabels: MREVRANGE_WITHLABELS,
|
||||
MREVRANGE,
|
||||
mRevRange: MREVRANGE,
|
||||
QUERYINDEX,
|
||||
queryIndex: QUERYINDEX,
|
||||
RANGE,
|
||||
range: RANGE,
|
||||
REVRANGE,
|
||||
revRange: REVRANGE
|
||||
} as const satisfies RedisCommands;
|
||||
|
||||
export enum TimeSeriesAggregationType {
|
||||
AVG = 'AVG',
|
||||
// @deprecated
|
||||
AVERAGE = 'AVG',
|
||||
FIRST = 'FIRST',
|
||||
LAST = 'LAST',
|
||||
MIN = 'MIN',
|
||||
// @deprecated
|
||||
MINIMUM = 'MIN',
|
||||
MAX = 'MAX',
|
||||
// @deprecated
|
||||
MAXIMUM = 'MAX',
|
||||
SUM = 'SUM',
|
||||
RANGE = 'RANGE',
|
||||
COUNT = 'COUNT',
|
||||
STD_P = 'STD.P',
|
||||
STD_S = 'STD.S',
|
||||
VAR_P = 'VAR.P',
|
||||
VAR_S = 'VAR.S',
|
||||
TWA = 'TWA'
|
||||
export function pushIgnoreArgument(args: Array<RedisArgument>, ignore?: TsIgnoreOptions) {
|
||||
if (ignore !== undefined) {
|
||||
args.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export enum TimeSeriesDuplicatePolicies {
|
||||
BLOCK = 'BLOCK',
|
||||
FIRST = 'FIRST',
|
||||
LAST = 'LAST',
|
||||
MIN = 'MIN',
|
||||
MAX = 'MAX',
|
||||
SUM = 'SUM'
|
||||
export function pushRetentionArgument(args: Array<RedisArgument>, retention?: number) {
|
||||
if (retention !== undefined) {
|
||||
args.push('RETENTION', retention.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export enum TimeSeriesReducers {
|
||||
AVG = 'AVG',
|
||||
SUM = 'SUM',
|
||||
MIN = 'MIN',
|
||||
// @deprecated
|
||||
MINIMUM = 'MIN',
|
||||
MAX = 'MAX',
|
||||
// @deprecated
|
||||
MAXIMUM = 'MAX',
|
||||
RANGE = 'range',
|
||||
COUNT = 'COUNT',
|
||||
STD_P = 'STD.P',
|
||||
STD_S = 'STD.S',
|
||||
VAR_P = 'VAR.P',
|
||||
VAR_S = 'VAR.S',
|
||||
export const TIME_SERIES_ENCODING = {
|
||||
COMPRESSED: 'COMPRESSED',
|
||||
UNCOMPRESSED: 'UNCOMPRESSED'
|
||||
} as const;
|
||||
|
||||
export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING];
|
||||
|
||||
export function pushEncodingArgument(args: Array<RedisArgument>, encoding?: TimeSeriesEncoding) {
|
||||
if (encoding !== undefined) {
|
||||
args.push('ENCODING', encoding);
|
||||
}
|
||||
}
|
||||
|
||||
export function pushChunkSizeArgument(args: Array<RedisArgument>, chunkSize?: number) {
|
||||
if (chunkSize !== undefined) {
|
||||
args.push('CHUNK_SIZE', chunkSize.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export const TIME_SERIES_DUPLICATE_POLICIES = {
|
||||
BLOCK: 'BLOCK',
|
||||
FIRST: 'FIRST',
|
||||
LAST: 'LAST',
|
||||
MIN: 'MIN',
|
||||
MAX: 'MAX',
|
||||
SUM: 'SUM'
|
||||
} as const;
|
||||
|
||||
export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES];
|
||||
|
||||
export function pushDuplicatePolicy(args: Array<RedisArgument>, duplicatePolicy?: TimeSeriesDuplicatePolicies) {
|
||||
if (duplicatePolicy !== undefined) {
|
||||
args.push('DUPLICATE_POLICY', duplicatePolicy);
|
||||
}
|
||||
}
|
||||
|
||||
export type Timestamp = number | Date | string;
|
||||
|
||||
export function transformTimestampArgument(timestamp: Timestamp): string {
|
||||
if (typeof timestamp === 'string') return timestamp;
|
||||
if (typeof timestamp === 'string') return timestamp;
|
||||
|
||||
return (
|
||||
typeof timestamp === 'number' ?
|
||||
timestamp :
|
||||
timestamp.getTime()
|
||||
).toString();
|
||||
return (
|
||||
typeof timestamp === 'number' ?
|
||||
timestamp :
|
||||
timestamp.getTime()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function pushIgnoreArgument(args: RedisCommandArguments, ignore?: ADD.TsIgnoreOptions) {
|
||||
if (ignore !== undefined) {
|
||||
args.push('IGNORE', ignore.MAX_TIME_DIFF.toString(), ignore.MAX_VAL_DIFF.toString());
|
||||
export type Labels = {
|
||||
[label: string]: string;
|
||||
};
|
||||
|
||||
export function pushLabelsArgument(args: Array<RedisArgument>, labels?: Labels) {
|
||||
if (labels) {
|
||||
args.push('LABELS');
|
||||
|
||||
for (const [label, value] of Object.entries(labels)) {
|
||||
args.push(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>;
|
||||
|
||||
export const transformSampleReply = {
|
||||
2(reply: Resp2Reply<SampleRawReply>) {
|
||||
const [ timestamp, value ] = reply as unknown as UnwrapReply<typeof reply>;
|
||||
return {
|
||||
timestamp,
|
||||
value: Number(value) // TODO: use double type mapping instead
|
||||
};
|
||||
},
|
||||
3(reply: SampleRawReply) {
|
||||
const [ timestamp, value ] = reply as unknown as UnwrapReply<typeof reply>;
|
||||
return {
|
||||
timestamp,
|
||||
value
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export type SamplesRawReply = ArrayReply<SampleRawReply>;
|
||||
|
||||
export const transformSamplesReply = {
|
||||
2(reply: Resp2Reply<SamplesRawReply>) {
|
||||
return (reply as unknown as UnwrapReply<typeof reply>)
|
||||
.map(sample => transformSampleReply[2](sample));
|
||||
},
|
||||
3(reply: SamplesRawReply) {
|
||||
return (reply as unknown as UnwrapReply<typeof reply>)
|
||||
.map(sample => transformSampleReply[3](sample));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: move to @redis/client?
|
||||
export function resp2MapToValue<
|
||||
RAW_VALUE extends TuplesReply<[key: BlobStringReply, ...rest: Array<ReplyUnion>]>,
|
||||
TRANSFORMED
|
||||
>(
|
||||
wrappedReply: ArrayReply<RAW_VALUE>,
|
||||
parseFunc: (rawValue: UnwrapReply<RAW_VALUE>) => TRANSFORMED,
|
||||
typeMapping?: TypeMapping
|
||||
): MapReply<BlobStringReply, TRANSFORMED> {
|
||||
const reply = wrappedReply as unknown as UnwrapReply<typeof wrappedReply>;
|
||||
switch (typeMapping?.[RESP_TYPES.MAP]) {
|
||||
case Map: {
|
||||
const ret = new Map<string, TRANSFORMED>();
|
||||
for (const wrappedTuple of reply) {
|
||||
const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
|
||||
const key = tuple[0] as unknown as UnwrapReply<typeof tuple[0]>;
|
||||
ret.set(key.toString(), parseFunc(tuple));
|
||||
}
|
||||
return ret as never;
|
||||
}
|
||||
case Array: {
|
||||
for (const wrappedTuple of reply) {
|
||||
const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
|
||||
(tuple[1] as unknown as TRANSFORMED) = parseFunc(tuple);
|
||||
}
|
||||
return reply as never;
|
||||
}
|
||||
default: {
|
||||
const ret: Record<string, TRANSFORMED> = Object.create(null);
|
||||
for (const wrappedTuple of reply) {
|
||||
const tuple = wrappedTuple as unknown as UnwrapReply<typeof wrappedTuple>;
|
||||
const key = tuple[0] as unknown as UnwrapReply<typeof tuple[0]>;
|
||||
ret[key.toString()] = parseFunc(tuple);
|
||||
}
|
||||
return ret as never;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resp3MapToValue<
|
||||
RAW_VALUE extends RespType<any, any, any, any>, // TODO: simplify types
|
||||
TRANSFORMED
|
||||
>(
|
||||
wrappedReply: MapReply<BlobStringReply, RAW_VALUE>,
|
||||
parseFunc: (rawValue: UnwrapReply<RAW_VALUE>) => TRANSFORMED
|
||||
): MapReply<BlobStringReply, TRANSFORMED> {
|
||||
const reply = wrappedReply as unknown as UnwrapReply<typeof wrappedReply>;
|
||||
if (reply instanceof Array) {
|
||||
for (let i = 1; i < reply.length; i += 2) {
|
||||
(reply[i] as unknown as TRANSFORMED) = parseFunc(reply[i] as unknown as UnwrapReply<RAW_VALUE>);
|
||||
}
|
||||
} else if (reply instanceof Map) {
|
||||
for (const [key, value] of reply.entries()) {
|
||||
(reply as unknown as Map<BlobStringReply, TRANSFORMED>).set(
|
||||
key,
|
||||
parseFunc(value as unknown as UnwrapReply<typeof value>)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(reply)) {
|
||||
(reply[key] as unknown as TRANSFORMED) = parseFunc(value as unknown as UnwrapReply<typeof value>);
|
||||
}
|
||||
}
|
||||
return reply as never;
|
||||
}
|
||||
|
||||
export function pushSelectedLabelsArguments(
|
||||
args: Array<RedisArgument>,
|
||||
selectedLabels: RedisVariadicArgument
|
||||
) {
|
||||
args.push('SELECTED_LABELS');
|
||||
return pushVariadicArguments(args, selectedLabels);
|
||||
}
|
||||
|
||||
export type RawLabelValue = BlobStringReply | NullReply;
|
||||
|
||||
export type RawLabels<T extends RawLabelValue> = ArrayReply<TuplesReply<[
|
||||
label: BlobStringReply,
|
||||
value: T
|
||||
]>>;
|
||||
|
||||
export function transformRESP2Labels<T extends RawLabelValue>(
|
||||
labels: RawLabels<T>,
|
||||
typeMapping?: TypeMapping
|
||||
): MapReply<BlobStringReply, T> {
|
||||
const unwrappedLabels = labels as unknown as UnwrapReply<typeof labels>;
|
||||
switch (typeMapping?.[RESP_TYPES.MAP]) {
|
||||
case Map:
|
||||
const map = new Map<string, T>();
|
||||
for (const tuple of unwrappedLabels) {
|
||||
const [key, value] = tuple as unknown as UnwrapReply<typeof tuple>;
|
||||
const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
|
||||
map.set(unwrappedKey.toString(), value);
|
||||
}
|
||||
return map as never;
|
||||
|
||||
case Array:
|
||||
return unwrappedLabels.flat() as never;
|
||||
|
||||
case Object:
|
||||
default:
|
||||
const labelsObject: Record<string, T> = Object.create(null);
|
||||
for (const tuple of unwrappedLabels) {
|
||||
const [key, value] = tuple as unknown as UnwrapReply<typeof tuple>;
|
||||
const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
|
||||
labelsObject[unwrappedKey.toString()] = value;
|
||||
}
|
||||
return labelsObject as never;
|
||||
}
|
||||
}
|
||||
|
||||
export function pushRetentionArgument(args: RedisCommandArguments, retention?: number): RedisCommandArguments {
|
||||
if (retention !== undefined) {
|
||||
args.push(
|
||||
'RETENTION',
|
||||
retention.toString()
|
||||
);
|
||||
export function transformRESP2LabelsWithSources<T extends RawLabelValue>(
|
||||
labels: RawLabels<T>,
|
||||
typeMapping?: TypeMapping
|
||||
) {
|
||||
const unwrappedLabels = labels as unknown as UnwrapReply<typeof labels>;
|
||||
const to = unwrappedLabels.length - 2; // ignore __reducer__ and __source__
|
||||
let transformedLabels: MapReply<BlobStringReply, T>;
|
||||
switch (typeMapping?.[RESP_TYPES.MAP]) {
|
||||
case Map:
|
||||
const map = new Map<string, T>();
|
||||
for (let i = 0; i < to; i++) {
|
||||
const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply<typeof unwrappedLabels[number]>;
|
||||
const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
|
||||
map.set(unwrappedKey.toString(), value);
|
||||
}
|
||||
transformedLabels = map as never;
|
||||
break;
|
||||
|
||||
case Array:
|
||||
transformedLabels = unwrappedLabels.slice(0, to).flat() as never;
|
||||
break;
|
||||
|
||||
case Object:
|
||||
default:
|
||||
const labelsObject: Record<string, T> = Object.create(null);
|
||||
for (let i = 0; i < to; i++) {
|
||||
const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply<typeof unwrappedLabels[number]>;
|
||||
const unwrappedKey = key as unknown as UnwrapReply<typeof key>;
|
||||
labelsObject[unwrappedKey.toString()] = value;
|
||||
}
|
||||
transformedLabels = labelsObject as never;
|
||||
break;
|
||||
}
|
||||
|
||||
const sourcesTuple = unwrappedLabels[unwrappedLabels.length - 1];
|
||||
const unwrappedSourcesTuple = sourcesTuple as unknown as UnwrapReply<typeof sourcesTuple>;
|
||||
// the __source__ label will never be null
|
||||
const transformedSources = transformRESP2Sources(unwrappedSourcesTuple[1] as BlobStringReply);
|
||||
|
||||
return {
|
||||
labels: transformedLabels,
|
||||
sources: transformedSources
|
||||
};
|
||||
}
|
||||
|
||||
function transformRESP2Sources(sourcesRaw: BlobStringReply) {
|
||||
// if a label contains "," this function will produce incorrcet results..
|
||||
// there is not much we can do about it, and we assume most users won't be using "," in their labels..
|
||||
|
||||
const unwrappedSources = sourcesRaw as unknown as UnwrapReply<typeof sourcesRaw>;
|
||||
if (typeof unwrappedSources === 'string') {
|
||||
return unwrappedSources.split(',');
|
||||
}
|
||||
|
||||
const indexOfComma = unwrappedSources.indexOf(',');
|
||||
if (indexOfComma === -1) {
|
||||
return [unwrappedSources];
|
||||
}
|
||||
|
||||
const sourcesArray = [
|
||||
unwrappedSources.subarray(0, indexOfComma)
|
||||
];
|
||||
|
||||
let previousComma = indexOfComma + 1;
|
||||
while (true) {
|
||||
const indexOf = unwrappedSources.indexOf(',', previousComma);
|
||||
if (indexOf === -1) {
|
||||
sourcesArray.push(
|
||||
unwrappedSources.subarray(previousComma)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export enum TimeSeriesEncoding {
|
||||
COMPRESSED = 'COMPRESSED',
|
||||
UNCOMPRESSED = 'UNCOMPRESSED'
|
||||
}
|
||||
|
||||
export function pushEncodingArgument(args: RedisCommandArguments, encoding?: TimeSeriesEncoding): RedisCommandArguments {
|
||||
if (encoding !== undefined) {
|
||||
args.push(
|
||||
'ENCODING',
|
||||
encoding
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function pushChunkSizeArgument(args: RedisCommandArguments, chunkSize?: number): RedisCommandArguments {
|
||||
if (chunkSize !== undefined) {
|
||||
args.push(
|
||||
'CHUNK_SIZE',
|
||||
chunkSize.toString()
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function pushDuplicatePolicy(args: RedisCommandArguments, duplicatePolicy?: TimeSeriesDuplicatePolicies): RedisCommandArguments {
|
||||
if (duplicatePolicy !== undefined) {
|
||||
args.push(
|
||||
'DUPLICATE_POLICY',
|
||||
duplicatePolicy
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export type RawLabels = Array<[label: string, value: string]>;
|
||||
|
||||
export type Labels = {
|
||||
[label: string]: string;
|
||||
};
|
||||
|
||||
export function transformLablesReply(reply: RawLabels): Labels {
|
||||
const labels: Labels = {};
|
||||
|
||||
for (const [key, value] of reply) {
|
||||
labels[key] = value;
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
export function pushLabelsArgument(args: RedisCommandArguments, labels?: Labels): RedisCommandArguments {
|
||||
if (labels) {
|
||||
args.push('LABELS');
|
||||
|
||||
for (const [label, value] of Object.entries(labels)) {
|
||||
args.push(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export interface IncrDecrOptions {
|
||||
TIMESTAMP?: Timestamp;
|
||||
RETENTION?: number;
|
||||
UNCOMPRESSED?: boolean;
|
||||
CHUNK_SIZE?: number;
|
||||
LABELS?: Labels;
|
||||
}
|
||||
|
||||
export function transformIncrDecrArguments(
|
||||
command: 'TS.INCRBY' | 'TS.DECRBY',
|
||||
key: string,
|
||||
value: number,
|
||||
options?: IncrDecrOptions
|
||||
): RedisCommandArguments {
|
||||
const args = [
|
||||
command,
|
||||
key,
|
||||
value.toString()
|
||||
];
|
||||
|
||||
if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) {
|
||||
args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP));
|
||||
}
|
||||
|
||||
pushRetentionArgument(args, options?.RETENTION);
|
||||
|
||||
if (options?.UNCOMPRESSED) {
|
||||
args.push('UNCOMPRESSED');
|
||||
}
|
||||
|
||||
pushChunkSizeArgument(args, options?.CHUNK_SIZE);
|
||||
|
||||
pushLabelsArgument(args, options?.LABELS);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export type SampleRawReply = [timestamp: number, value: string];
|
||||
|
||||
export interface SampleReply {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function transformSampleReply(reply: SampleRawReply): SampleReply {
|
||||
return {
|
||||
timestamp: reply[0],
|
||||
value: Number(reply[1])
|
||||
};
|
||||
}
|
||||
|
||||
export enum TimeSeriesBucketTimestamp {
|
||||
LOW = '-',
|
||||
HIGH = '+',
|
||||
MID = '~'
|
||||
}
|
||||
|
||||
export interface RangeOptions {
|
||||
LATEST?: boolean;
|
||||
FILTER_BY_TS?: Array<Timestamp>;
|
||||
FILTER_BY_VALUE?: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
COUNT?: number;
|
||||
ALIGN?: Timestamp;
|
||||
AGGREGATION?: {
|
||||
type: TimeSeriesAggregationType;
|
||||
timeBucket: Timestamp;
|
||||
BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp;
|
||||
EMPTY?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function pushRangeArguments(
|
||||
args: RedisCommandArguments,
|
||||
fromTimestamp: Timestamp,
|
||||
toTimestamp: Timestamp,
|
||||
options?: RangeOptions
|
||||
): RedisCommandArguments {
|
||||
args.push(
|
||||
transformTimestampArgument(fromTimestamp),
|
||||
transformTimestampArgument(toTimestamp)
|
||||
const source = unwrappedSources.subarray(
|
||||
previousComma,
|
||||
indexOf
|
||||
);
|
||||
sourcesArray.push(source);
|
||||
previousComma = indexOf + 1;
|
||||
}
|
||||
|
||||
pushLatestArgument(args, options?.LATEST);
|
||||
|
||||
if (options?.FILTER_BY_TS) {
|
||||
args.push('FILTER_BY_TS');
|
||||
for (const ts of options.FILTER_BY_TS) {
|
||||
args.push(transformTimestampArgument(ts));
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.FILTER_BY_VALUE) {
|
||||
args.push(
|
||||
'FILTER_BY_VALUE',
|
||||
options.FILTER_BY_VALUE.min.toString(),
|
||||
options.FILTER_BY_VALUE.max.toString()
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.COUNT) {
|
||||
args.push(
|
||||
'COUNT',
|
||||
options.COUNT.toString()
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.ALIGN) {
|
||||
args.push(
|
||||
'ALIGN',
|
||||
transformTimestampArgument(options.ALIGN)
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.AGGREGATION) {
|
||||
args.push(
|
||||
'AGGREGATION',
|
||||
options.AGGREGATION.type,
|
||||
transformTimestampArgument(options.AGGREGATION.timeBucket)
|
||||
);
|
||||
|
||||
if (options.AGGREGATION.BUCKETTIMESTAMP) {
|
||||
args.push(
|
||||
'BUCKETTIMESTAMP',
|
||||
options.AGGREGATION.BUCKETTIMESTAMP
|
||||
);
|
||||
}
|
||||
|
||||
if (options.AGGREGATION.EMPTY) {
|
||||
args.push('EMPTY');
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
interface MRangeGroupBy {
|
||||
label: string;
|
||||
reducer: TimeSeriesReducers;
|
||||
}
|
||||
|
||||
export function pushMRangeGroupByArguments(args: RedisCommandArguments, groupBy?: MRangeGroupBy): RedisCommandArguments {
|
||||
if (groupBy) {
|
||||
args.push(
|
||||
'GROUPBY',
|
||||
groupBy.label,
|
||||
'REDUCE',
|
||||
groupBy.reducer
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export type Filter = string | Array<string>;
|
||||
|
||||
export function pushFilterArgument(args: RedisCommandArguments, filter: string | Array<string>): RedisCommandArguments {
|
||||
args.push('FILTER');
|
||||
return pushVerdictArguments(args, filter);
|
||||
}
|
||||
|
||||
export interface MRangeOptions extends RangeOptions {
|
||||
GROUPBY?: MRangeGroupBy;
|
||||
}
|
||||
|
||||
export function pushMRangeArguments(
|
||||
args: RedisCommandArguments,
|
||||
fromTimestamp: Timestamp,
|
||||
toTimestamp: Timestamp,
|
||||
filter: Filter,
|
||||
options?: MRangeOptions
|
||||
): RedisCommandArguments {
|
||||
args = pushRangeArguments(args, fromTimestamp, toTimestamp, options);
|
||||
args = pushFilterArgument(args, filter);
|
||||
return pushMRangeGroupByArguments(args, options?.GROUPBY);
|
||||
}
|
||||
|
||||
export type SelectedLabels = string | Array<string>;
|
||||
|
||||
export function pushWithLabelsArgument(args: RedisCommandArguments, selectedLabels?: SelectedLabels): RedisCommandArguments {
|
||||
if (!selectedLabels) {
|
||||
args.push('WITHLABELS');
|
||||
} else {
|
||||
args.push('SELECTED_LABELS');
|
||||
args = pushVerdictArguments(args, selectedLabels);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export interface MRangeWithLabelsOptions extends MRangeOptions {
|
||||
SELECTED_LABELS?: SelectedLabels;
|
||||
}
|
||||
|
||||
export function pushMRangeWithLabelsArguments(
|
||||
args: RedisCommandArguments,
|
||||
fromTimestamp: Timestamp,
|
||||
toTimestamp: Timestamp,
|
||||
filter: Filter,
|
||||
options?: MRangeWithLabelsOptions
|
||||
): RedisCommandArguments {
|
||||
args = pushRangeArguments(args, fromTimestamp, toTimestamp, options);
|
||||
args = pushWithLabelsArgument(args, options?.SELECTED_LABELS);
|
||||
args = pushFilterArgument(args, filter);
|
||||
return pushMRangeGroupByArguments(args, options?.GROUPBY);
|
||||
}
|
||||
|
||||
export function transformRangeReply(reply: Array<SampleRawReply>): Array<SampleReply> {
|
||||
return reply.map(transformSampleReply);
|
||||
}
|
||||
|
||||
type MRangeRawReply = Array<[
|
||||
key: string,
|
||||
labels: RawLabels,
|
||||
samples: Array<SampleRawReply>
|
||||
]>;
|
||||
|
||||
interface MRangeReplyItem {
|
||||
key: string;
|
||||
samples: Array<SampleReply>;
|
||||
}
|
||||
|
||||
export function transformMRangeReply(reply: MRangeRawReply): Array<MRangeReplyItem> {
|
||||
const args = [];
|
||||
|
||||
for (const [key, _, sample] of reply) {
|
||||
args.push({
|
||||
key,
|
||||
samples: sample.map(transformSampleReply)
|
||||
});
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
export interface MRangeWithLabelsReplyItem extends MRangeReplyItem {
|
||||
labels: Labels;
|
||||
}
|
||||
|
||||
export function transformMRangeWithLabelsReply(reply: MRangeRawReply): Array<MRangeWithLabelsReplyItem> {
|
||||
const args = [];
|
||||
|
||||
for (const [key, labels, samples] of reply) {
|
||||
args.push({
|
||||
key,
|
||||
labels: transformLablesReply(labels),
|
||||
samples: samples.map(transformSampleReply)
|
||||
});
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function pushLatestArgument(args: RedisCommandArguments, latest?: boolean): RedisCommandArguments {
|
||||
if (latest) {
|
||||
args.push('LATEST');
|
||||
}
|
||||
|
||||
return args;
|
||||
return sourcesArray;
|
||||
}
|
||||
|
Reference in New Issue
Block a user