{"version":3,"sources":["../src/config.ts","../src/push.ts","../src/types/index.ts","../src/index.ts"],"sourcesContent":["import type {\n  Config,\n  Settings,\n  RedisSettings,\n  PartialConfig,\n  Env,\n} from './types';\nimport type { Logger } from '@walkeros/core';\nimport { isObject } from '@walkeros/core';\n\nexport function getConfig(\n  partialConfig: PartialConfig = {},\n  logger: Logger.Instance,\n): Config {\n  const raw = (partialConfig.settings ?? {}) as Partial<Settings>;\n  const redis: Partial<RedisSettings> =\n    raw.redis ?? ({} as Partial<RedisSettings>);\n\n  if (!redis.streamKey) {\n    logger.throw('Config settings redis.streamKey missing');\n  }\n\n  const redisSettings: RedisSettings = {\n    ...redis,\n    streamKey: redis.streamKey as string,\n    serialization: redis.serialization ?? 'json',\n  };\n\n  const settings: Settings = { redis: redisSettings };\n\n  return { ...partialConfig, settings };\n}\n\nexport function isRedisEnv(env: unknown): env is Env {\n  if (!isObject(env)) return false;\n  const maybe = env as { Redis?: { Client?: unknown } };\n  return typeof maybe.Redis?.Client === 'function';\n}\n","import type {\n  PushFn,\n  RedisSettings,\n  RedisClientMock,\n  XaddArg,\n  SerializationMode,\n} from './types';\nimport { isString } from '@walkeros/core';\n\nexport const push: PushFn = async function (event, { config, rule, logger }) {\n  const settings = config.settings as { redis?: RedisSettings } | undefined;\n  const redis: RedisSettings | undefined = settings?.redis;\n\n  if (!redis) {\n    logger.warn('Redis settings missing');\n    return;\n  }\n\n  const client = redis._client;\n  if (!client) {\n    logger.warn('Redis client not initialized');\n    return;\n  }\n\n  // Derive stream key: rule override -> destination default\n  const ruleSettings = rule?.settings ?? {};\n  const streamKey = isString(ruleSettings.streamKey)\n    ? ruleSettings.streamKey\n    : redis.streamKey;\n\n  const serialization: SerializationMode = redis.serialization ?? 'json';\n\n  // Serialize event\n  const fields: string[] =\n    serialization === 'flat'\n      ? flattenEvent(event as unknown as Record<string, unknown>)\n      : ['event', JSON.stringify(event)];\n\n  // Build XADD arguments\n  const args: XaddArg[] = [streamKey];\n\n  // Optional MAXLEN trimming\n  if (redis.maxLen) {\n    args.push('MAXLEN');\n    if (!redis.exactTrimming) args.push('~');\n    args.push(redis.maxLen);\n  }\n\n  args.push('*'); // Auto-generate entry ID\n  args.push(...fields); // Field-value pairs\n\n  logger.debug('Redis XADD', { stream: streamKey });\n\n  try {\n    const entryId = await (client as RedisClientMock).xadd(...args);\n    logger.debug('Redis XADD complete', { stream: streamKey, entryId });\n  } catch (error) {\n    logger.error('Redis XADD failed', {\n      stream: streamKey,\n      error: error instanceof Error ? error.message : String(error),\n    });\n  }\n};\n\nfunction flattenEvent(event: Record<string, unknown>): string[] {\n  const fields: string[] = [];\n  for (const [key, value] of Object.entries(event)) {\n    fields.push(\n      key,\n      typeof value === 'object' && value !== null\n        ? JSON.stringify(value)\n        : String(value),\n    );\n  }\n  return fields;\n}\n","import type { Destination as CoreDestination } from '@walkeros/core';\nimport type { DestinationServer } from '@walkeros/server-core';\n\n/**\n * Arguments passed to Redis XADD. Strings or numbers (for MAXLEN counts).\n */\nexport type XaddArg = string | number;\n\n/**\n * Mock-friendly Redis pipeline interface. Accumulates commands and\n * executes them with a single round-trip via exec().\n */\nexport interface RedisPipelineMock {\n  xadd: (...args: XaddArg[]) => RedisPipelineMock;\n  exec: () => Promise<Array<[Error | null, unknown]> | null>;\n}\n\n/**\n * Mock-friendly Redis client interface used by the destination.\n * Tests provide this via env.Redis; production creates a real ioredis\n * client and uses it directly.\n */\nexport interface RedisClientMock {\n  xadd: (...args: XaddArg[]) => Promise<string | null>;\n  pipeline: () => RedisPipelineMock;\n  quit: () => Promise<string>;\n  on?: (event: string, listener: (...args: unknown[]) => void) => unknown;\n}\n\n/**\n * Constructor signature for the Redis client. Accepts either a URL\n * string or an options object, matching ioredis's dual signature.\n */\nexport interface RedisClientConstructor {\n  new (url: string): RedisClientMock;\n  new (options: RedisClientOptions): RedisClientMock;\n}\n\n/**\n * Minimal ioredis options subset the destination passes through.\n * Unknown options are preserved for the SDK to handle.\n */\nexport interface RedisClientOptions {\n  host?: string;\n  port?: number;\n  username?: string;\n  password?: string;\n  db?: number;\n  tls?: boolean | Record<string, unknown>;\n  connectTimeout?: number;\n  commandTimeout?: number;\n  [key: string]: unknown;\n}\n\nexport type SerializationMode = 'json' | 'flat';\n\nexport interface RedisSettings {\n  /** Redis stream key name (e.g. 'walkeros:events'). */\n  streamKey: string;\n  /** Redis connection URL (e.g. 'redis://localhost:6379' or 'rediss://...'). */\n  url?: string;\n  /** ioredis connection options (used if no url provided). */\n  options?: RedisClientOptions;\n  /**\n   * Maximum stream length. Enables MAXLEN trimming on every XADD.\n   * Uses approximate (~) trimming by default for performance.\n   * Omit for unlimited stream length.\n   */\n  maxLen?: number;\n  /**\n   * Use exact MAXLEN instead of approximate (~).\n   * Not recommended for production -- significantly slower.\n   * Default: false (approximate trimming).\n   */\n  exactTrimming?: boolean;\n  /**\n   * Serialization mode for the event payload.\n   * - 'json': Single 'event' field with JSON string (default)\n   * - 'flat': Top-level event fields as separate stream fields\n   */\n  serialization?: SerializationMode;\n\n  // Runtime -- set during init, not user-facing\n  _client?: RedisClientMock;\n  _ownedClient?: boolean;\n}\n\nexport interface Settings {\n  redis: RedisSettings;\n}\n\nexport type InitSettings = Partial<Settings>;\n\nexport interface Mapping {\n  /** Override stream key for this rule. */\n  streamKey?: string;\n}\n\n/**\n * Env -- optional Redis SDK override. Production leaves this undefined\n * and the destination creates real ioredis client instances. Tests provide\n * mocks via env.Redis.\n */\nexport interface Env extends DestinationServer.Env {\n  Redis?: {\n    Client: RedisClientConstructor;\n  };\n}\n\nexport type Types = CoreDestination.Types<Settings, Mapping, Env, InitSettings>;\n\nexport interface Destination extends DestinationServer.Destination<Types> {\n  init: DestinationServer.InitFn<Types>;\n}\n\nexport type Config = {\n  settings: Settings;\n} & DestinationServer.Config<Types>;\n\nexport type InitFn = DestinationServer.InitFn<Types>;\nexport type PushFn = DestinationServer.PushFn<Types>;\nexport type PartialConfig = DestinationServer.PartialConfig<Types>;\nexport type PushEvents = DestinationServer.PushEvents<Mapping>;\n","import type {\n  Destination,\n  Settings,\n  Env,\n  RedisClientMock,\n  RedisClientConstructor,\n  RedisClientOptions,\n} from './types';\nimport { getConfig, isRedisEnv } from './config';\nimport { push } from './push';\n\n// Types re-export\nexport * as DestinationRedis from './types';\n\nexport const destinationRedis: Destination = {\n  type: 'redis',\n\n  config: {},\n\n  async init({ config: partialConfig, logger, env }) {\n    const config = getConfig(partialConfig, logger);\n    const settings = config.settings as Settings;\n    const redis = settings.redis;\n\n    // Skip creation if a client has already been wired in (testing).\n    if (redis._client) return config;\n\n    let Constructor: RedisClientConstructor | undefined;\n\n    // Prefer env-injected constructor (testing, dependency injection).\n    if (isRedisEnv(env)) {\n      const envTyped = env as Env;\n      Constructor = envTyped.Redis?.Client;\n    }\n\n    // Production path: load real ioredis SDK.\n    if (!Constructor) {\n      try {\n        // Use dynamic require to allow tests to mock via jest.mock('ioredis').\n        const ioredis = require('ioredis') as {\n          default?: RedisClientConstructor;\n          Redis?: RedisClientConstructor;\n        };\n        // ioredis exports the class as default (ESM) and as named Redis export.\n        Constructor = ioredis.default ?? ioredis.Redis;\n      } catch (err) {\n        logger.throw(`Failed to load ioredis: ${String(err)}`);\n        return config;\n      }\n    }\n\n    if (!Constructor) {\n      logger.throw('ioredis constructor not found');\n      return config;\n    }\n\n    let client: RedisClientMock;\n    if (redis.url) {\n      client = new Constructor(redis.url);\n    } else {\n      client = new Constructor(redis.options ?? ({} as RedisClientOptions));\n    }\n\n    redis._client = client;\n    redis._ownedClient = true;\n\n    return config;\n  },\n\n  async push(event, context) {\n    return await push(event, context);\n  },\n\n  async destroy({ config }) {\n    const settings = config?.settings as Settings | undefined;\n    const redis = settings?.redis;\n    if (!redis) return;\n\n    const client = redis._client;\n    // Only close clients the destination created (not user-provided)\n    if (client && redis._ownedClient) {\n      try {\n        await client.quit();\n      } finally {\n        redis._client = undefined;\n        redis._ownedClient = undefined;\n      }\n    }\n  },\n};\n\nexport default destinationRedis;\n"],"mappings":"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","names":[]}