UNPKG

@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
/** * 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[];