@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
135 lines (134 loc) • 6.44 kB
TypeScript
/**
* Redis Utilities for NeuroLink
* Helper functions for Redis storage operations
*/
import type { ChatMessage, RedisClient, RedisConversationObject, RedisStorageConfig } from "../types/index.js";
/**
* Redact the userinfo of every `scheme://user:pass@host` in `text`.
*
* Deliberately not a regex. `/:\/\/[^@]+@/` and its relatives backtrack
* quadratically on input with many `://` and no `@` (CodeQL
* js/polynomial-redos), and the URL here can come straight from a caller —
* `RedisStorageConfig.url` reaches this through `RedisArtifactStore` and the
* conversation memory manager. One forward pass, every character visited a
* bounded number of times.
*
* Redacts everything before the LAST `@` of the authority, so a password
* containing `@` is still hidden. A URL with only a user name is redacted
* too; the old regexes let that through.
*/
export declare function redactUrlCredentials(text: string): string;
/**
* Get a pooled Redis connection. Multiple callers with the same host:port:db
* share a single connection, reducing connection count.
*/
export declare function getPooledRedisClient(config: Required<RedisStorageConfig>): Promise<RedisClient>;
/**
* Release a pooled Redis connection. Only closes when refCount reaches 0.
*/
export declare function releasePooledRedisClient(config: Required<RedisStorageConfig>): Promise<void>;
/**
* Get stats about the connection pool
*/
export declare function getPoolStats(): Array<{
key: string;
refCount: number;
isOpen: boolean;
}>;
/**
* Creates a Redis client with the provided configuration
*/
export declare function createRedisClient(config: Required<RedisStorageConfig>): Promise<RedisClient>;
/**
* Generates a Redis key for session messages
*/
export declare function getSessionKey(config: Required<RedisStorageConfig>, sessionId: string, userId?: string): string;
/**
* Generates a Redis key for user sessions mapping
*/
export declare function getUserSessionsKey(config: Required<RedisStorageConfig>, userId: string): string;
/**
* Serializes conversation object for Redis storage
*/
export declare function serializeConversation(conversation: RedisConversationObject): string;
/**
* True for a complete `ChatMessage` — `id` included, because callers keying
* summary pointers and condensation groups off it are entitled to find one.
*/
export declare function isStoredChatMessage(value: unknown): value is ChatMessage;
/**
* Coerce a stored entry into a complete `ChatMessage`, or `undefined` when its
* shape is unusable.
*
* Shared by BOTH read paths on purpose. Inline blobs have always been
* validated; once messages moved into the companion LIST the split read
* bypassed that check, so `null`, a number or a bare string survived
* `JSON.parse` and reached callers that dereference `.role`, `.content` and
* `.metadata`. The two storage formats must offer the same read guarantee.
*
* `id` is a separate matter: it is required on `ChatMessage`, but no read path
* has ever enforced it, so history written before it existed carries none.
* Rejecting those records would empty otherwise-healthy sessions, so a missing
* id is backfilled rather than fatal — prefixed, so a synthesized id is never
* mistaken for one an existing pointer could reference.
*/
export declare function normalizeStoredMessage(value: unknown): ChatMessage | undefined;
/**
* Deserializes conversation object from Redis storage
*/
export declare function deserializeConversation(data: string | Buffer | null): RedisConversationObject | null;
/**
* Checks if Redis client is healthy
*/
export declare function isRedisHealthy(client: RedisClient): Promise<boolean>;
/**
* Scan Redis keys matching a pattern without blocking the server
* This is a non-blocking alternative to the KEYS command
*
* @param client Redis client
* @param pattern Pattern to match keys (e.g. "prefix:*")
* @param batchSize Number of keys to scan in each iteration (default: 100)
* @returns Array of keys matching the pattern
*/
export declare function scanKeys(client: RedisClient, pattern: string, batchSize?: number): Promise<string[]>;
/**
* Get normalized Redis configuration with defaults
*/
export declare function getNormalizedConfig(config: RedisStorageConfig): Required<RedisStorageConfig>;
/**
* Suffix of the companion LIST key holding a session's messages.
*
* Every `storeConversationTurn` used to re-serialize and SET the ENTIRE
* conversation, so per-turn write cost grew with history size. Measured
* against local Redis: 200 turns of 2KB messages stayed flat at 2ms, but 400
* turns of 20KB messages (~16MB blob) went 2ms -> 61ms, a 30x degradation on
* exactly the agentic tool-output profile. Splitting messages into an
* append-only LIST makes the per-turn write O(1) in conversation size.
*/
export declare const MESSAGES_KEY_SUFFIX = ":msgs";
/**
* Marker on a stored blob meaning "messages live in the companion LIST".
* A blob WITHOUT it is a legacy record whose inline `messages` array is
* authoritative — it is read as-is and converted on its next write, which is
* what makes this migration backward compatible.
*/
export declare const MESSAGES_IN_LIST_MARKER = "__nlMessagesInList";
/** Redis key holding a session's messages as an append-only LIST. */
export declare function getSessionMessagesKey(config: Required<RedisStorageConfig>, sessionId: string, userId?: string): string;
/**
* True for a companion messages LIST key.
*
* Scan-then-GET paths (`getStats`, session listing) match `${keyPrefix}*`, so
* they now also see these LIST keys — and `GET` on a LIST raises WRONGTYPE.
* They must filter with this, exactly as they already skip `:sessions` index
* keys.
*/
export declare function isSessionMessagesKey(key: string): boolean;
/** Serialize the conversation WITHOUT its messages, flagged for split reads. */
export declare function serializeConversationMetadata(conversation: RedisConversationObject): string;
/** True when a deserialized blob's messages live in the companion LIST. */
export declare function usesSplitMessageStorage(conversation: RedisConversationObject | null | undefined): boolean;
/** Parse LRANGE entries, skipping any single entry that is not usable. */
export declare function parseStoredMessages(entries: string[]): ChatMessage[];
/** Encode messages for RPUSH. */
export declare function encodeStoredMessages(messages: ChatMessage[]): string[];