fluxer/packages/kv_client/src/KVCommandArguments.tsx
2026-02-17 12:22:36 +00:00

187 lines
4.9 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {KVClientError, KVClientErrorCode} from '@fluxer/kv_client/src/KVClientError';
export interface KVSetOptions {
ttlSeconds?: number;
useNx: boolean;
}
export interface KVRangeByScoreOptions {
limit?: {
offset: number;
count: number;
};
}
export function parseSetArguments(args: Array<string | number>): KVSetOptions {
let ttlSeconds: number | undefined;
let useNx = false;
let index = 0;
while (index < args.length) {
const current = args[index];
const flag = typeof current === 'string' ? current.toUpperCase() : null;
if (flag === 'EX') {
if (ttlSeconds !== undefined) {
throw createInvalidArgumentError('set supports EX only once');
}
ttlSeconds = parseNumberArgument(args[index + 1], 'EX value');
index += 2;
continue;
}
if (flag === 'NX') {
if (useNx) {
throw createInvalidArgumentError('set supports NX only once');
}
useNx = true;
index += 1;
continue;
}
throw createInvalidArgumentError(`set received unsupported argument: ${String(current)}`);
}
return {
ttlSeconds,
useNx,
};
}
export function createStringEntriesFromPairs(args: Array<string>): Array<{key: string; value: string}> {
if (args.length % 2 !== 0) {
throw createInvalidArgumentError('mset requires key/value pairs');
}
const entries: Array<{key: string; value: string}> = [];
for (let index = 0; index < args.length; index += 2) {
entries.push({
key: args[index],
value: args[index + 1],
});
}
return entries;
}
export function createZSetMembersFromScorePairs(
scoreMembers: Array<number | string>,
): Array<{score: number; value: string}> {
if (scoreMembers.length % 2 !== 0) {
throw createInvalidArgumentError('zadd requires score/member pairs');
}
const members: Array<{score: number; value: string}> = [];
for (let index = 0; index < scoreMembers.length; index += 2) {
const rawScore = scoreMembers[index];
const rawMember = scoreMembers[index + 1];
if (typeof rawMember !== 'string') {
throw createInvalidArgumentError('zadd member must be a string');
}
members.push({
score: parseFiniteNumber(rawScore, 'zadd score'),
value: rawMember,
});
}
return members;
}
export function normalizeScoreBound(score: string | number): string | number {
if (score === '-inf' || score === '+inf') {
return score;
}
return parseNumberArgument(score, 'score');
}
export function parseRangeByScoreArguments(args: Array<string | number>): KVRangeByScoreOptions {
let index = 0;
let limit: {offset: number; count: number} | undefined;
while (index < args.length) {
const token = args[index];
const keyword = typeof token === 'string' ? token.toUpperCase() : null;
if (keyword === 'LIMIT') {
if (limit) {
throw createInvalidArgumentError('zrangebyscore supports LIMIT only once');
}
const offset = parseNonNegativeInteger(args[index + 1], 'LIMIT offset');
const count = parseNonNegativeInteger(args[index + 2], 'LIMIT count');
limit = {offset, count};
index += 3;
continue;
}
throw createInvalidArgumentError(`zrangebyscore received unsupported argument: ${String(token)}`);
}
return {limit};
}
function parseNumberArgument(value: string | number | undefined, label: string): number {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim().length > 0) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
throw createInvalidArgumentError(`${label} must be a finite number`);
}
function parseFiniteNumber(value: string | number, label: string): number {
const parsed = parseNumberArgument(value, label);
if (!Number.isFinite(parsed)) {
throw createInvalidArgumentError(`${label} must be finite`);
}
return parsed;
}
function parseNonNegativeInteger(value: string | number | undefined, label: string): number {
const parsed = parseNumberArgument(value, label);
if (!Number.isInteger(parsed) || parsed < 0) {
throw createInvalidArgumentError(`${label} must be a non-negative integer`);
}
return parsed;
}
function createInvalidArgumentError(message: string): KVClientError {
return new KVClientError({
code: KVClientErrorCode.INVALID_ARGUMENT,
message,
});
}