{"version":3,"sources":["../../../../src/batch-settlement/server/redisStorage.ts"],"sourcesContent":["import type { Channel, ChannelStorage, ChannelUpdateResult } from \"./storage\";\n\nconst DEFAULT_KEY_PREFIX = \"x402:batch-settlement\";\nconst DEFAULT_LOCK_RETRY_INTERVAL_MS = 10;\nconst DEFAULT_SCAN_COUNT = 100;\n\nconst UPDATE_CHANNEL_SCRIPT = `\nlocal current = redis.call(\"GET\", KEYS[1])\nlocal expectedExists = ARGV[1]\nlocal expected = ARGV[2]\nlocal operation = ARGV[3]\nlocal nextValue = ARGV[4]\n\nif expectedExists == \"0\" then\n  if current ~= false then\n    return {0, current}\n  end\nelseif current ~= expected then\n  return {0, current or false}\nend\n\nif operation == \"delete\" then\n  redis.call(\"DEL\", KEYS[1])\n  return {1, false}\nend\n\nif operation == \"set\" then\n  redis.call(\"SET\", KEYS[1], nextValue)\n  return {1, nextValue}\nend\n\nreturn {1, current or false}\n`;\n\nexport type RedisEvalOptions = {\n  keys: string[];\n  arguments: string[];\n};\n\nexport type RedisSetOptions = {\n  NX?: true;\n  PX?: number;\n};\n\nexport type RedisScanOptions = {\n  MATCH?: string;\n  COUNT?: number;\n};\n\nexport type RedisChannelStorageClient = {\n  get(key: string): Promise<string | null>;\n  set(key: string, value: string, options?: RedisSetOptions): Promise<string | null>;\n  del(key: string): Promise<number>;\n  eval(script: string, options: RedisEvalOptions): Promise<unknown>;\n  scanIterator(options: RedisScanOptions): AsyncIterable<string | string[]>;\n};\n\nexport type RedisChannelStorageOptions = {\n  client: RedisChannelStorageClient;\n  keyPrefix?: string;\n  lockTtlMs?: number;\n  lockRetryIntervalMs?: number;\n  lockRenewalIntervalMs?: number;\n  scanCount?: number;\n};\n\ntype RedisUpdateOperation = \"delete\" | \"keep\" | \"set\";\n\ntype ParsedRedisUpdateResult = {\n  applied: boolean;\n};\n\n/**\n * Redis-backed {@link ChannelStorage} with optimistic atomic updates.\n */\nexport class RedisChannelStorage implements ChannelStorage {\n  private readonly client: RedisChannelStorageClient;\n  private readonly keyPrefix: string;\n  private readonly channelKeyPrefix: string;\n  private readonly lockRetryIntervalMs: number;\n  private readonly scanCount: number;\n\n  /**\n   * Creates Redis-backed server channel storage.\n   *\n   * @param options - Redis client and optional key/retry configuration.\n   */\n  constructor(options: RedisChannelStorageOptions) {\n    this.client = options.client;\n    this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;\n    this.channelKeyPrefix = `${this.keyPrefix}:server:channel`;\n    this.lockRetryIntervalMs = options.lockRetryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;\n    this.scanCount = options.scanCount ?? DEFAULT_SCAN_COUNT;\n  }\n\n  /**\n   * Loads a persisted channel record, if present.\n   *\n   * @param channelId - The channel identifier.\n   * @returns Parsed channel record or `undefined` when the key is missing.\n   */\n  async get(channelId: string): Promise<Channel | undefined> {\n    const raw = await this.client.get(this.channelKey(channelId));\n    if (!raw) return undefined;\n    return JSON.parse(raw) as Channel;\n  }\n\n  /**\n   * Lists all stored channel records by scanning Redis keys.\n   *\n   * @returns Channel records sorted by channelId.\n   */\n  async list(): Promise<Channel[]> {\n    const channels: Channel[] = [];\n    for await (const keyOrKeys of this.client.scanIterator({\n      MATCH: `${this.channelKeyPrefix}:*`,\n      COUNT: this.scanCount,\n    })) {\n      const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys];\n      for (const key of keys) {\n        if (key.endsWith(\":lock\")) continue;\n        const raw = await this.client.get(key);\n        if (!raw) continue;\n        channels.push(JSON.parse(raw) as Channel);\n      }\n    }\n    return channels.sort((a, b) => a.channelId.localeCompare(b.channelId));\n  }\n\n  /**\n   * Atomically inspects and mutates a channel record with Redis compare-and-write retries.\n   *\n   * @param channelId - The channel identifier.\n   * @param update - Mutation callback. Return `undefined` to delete, or `current` to leave unchanged.\n   * @returns The final stored channel and whether storage updated, stayed unchanged, or deleted.\n   */\n  async updateChannel(\n    channelId: string,\n    update: (current: Channel | undefined) => Channel | undefined,\n  ): Promise<ChannelUpdateResult> {\n    const key = this.channelKey(channelId);\n    while (true) {\n      const currentRaw = await this.client.get(key);\n      const current = currentRaw ? (JSON.parse(currentRaw) as Channel) : undefined;\n      const next = update(current);\n\n      if (next === current) {\n        const result = await this.commitUpdate(key, currentRaw, \"keep\");\n        if (result.applied) return { channel: current, status: \"unchanged\" };\n        await sleep(this.lockRetryIntervalMs);\n        continue;\n      }\n\n      if (!next) {\n        const result = await this.commitUpdate(key, currentRaw, \"delete\");\n        if (result.applied) {\n          return { channel: undefined, status: current ? \"deleted\" : \"unchanged\" };\n        }\n        await sleep(this.lockRetryIntervalMs);\n        continue;\n      }\n\n      const nextRaw = JSON.stringify(next);\n      const result = await this.commitUpdate(key, currentRaw, \"set\", nextRaw);\n      if (result.applied) return { channel: next, status: \"updated\" };\n      await sleep(this.lockRetryIntervalMs);\n    }\n  }\n\n  /**\n   * Applies a channel mutation only if the key still contains the value that was inspected.\n   *\n   * @param key - Redis channel key to mutate.\n   * @param expectedRaw - Raw JSON value observed before running the update callback.\n   * @param operation - Mutation to apply when the observed value is still current.\n   * @param nextRaw - Raw JSON value to write for set operations.\n   * @returns Whether the mutation was applied.\n   */\n  private async commitUpdate(\n    key: string,\n    expectedRaw: string | null,\n    operation: RedisUpdateOperation,\n    nextRaw = \"\",\n  ): Promise<ParsedRedisUpdateResult> {\n    return parseRedisUpdateResult(\n      await this.client.eval(UPDATE_CHANNEL_SCRIPT, {\n        keys: [key],\n        arguments: [expectedRaw === null ? \"0\" : \"1\", expectedRaw ?? \"\", operation, nextRaw],\n      }),\n    );\n  }\n\n  /**\n   * Builds the Redis key for a stored channel record.\n   *\n   * @param channelId - The channel identifier.\n   * @returns Redis key for the channel JSON.\n   */\n  private channelKey(channelId: string) {\n    return `${this.channelKeyPrefix}:${channelId.toLowerCase()}`;\n  }\n}\n\n/**\n * Parses the Redis script response.\n *\n * @param value - Raw response from the Redis client.\n * @returns Whether the compare-and-write applied.\n */\nfunction parseRedisUpdateResult(value: unknown): ParsedRedisUpdateResult {\n  if (!Array.isArray(value) || value.length < 1) {\n    throw new Error(\"Unexpected Redis update response\");\n  }\n\n  const [applied, raw] = value;\n  if (applied !== 0 && applied !== 1) {\n    throw new Error(\"Unexpected Redis update status\");\n  }\n\n  if (raw !== false && raw !== null && raw !== undefined && typeof raw !== \"string\") {\n    throw new Error(\"Unexpected Redis update value\");\n  }\n\n  return { applied: applied === 1 };\n}\n\n/**\n * Resolves after the requested delay.\n *\n * @param ms - Delay in milliseconds.\n * @returns Promise resolved after the delay.\n */\nfunction sleep(ms: number) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":["result"]}