[
  {
    "path": ".denov",
    "content": "2.7.7\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*.ts]\nindent_style = space\nindent_size = 2"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @uki00a\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [keroxp]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    cooldown:\n      default-days: 1\n"
  },
  {
    "path": ".github/pinact.yaml",
    "content": "# pinact - https://github.com/suzuki-shunsuke/pinact\nfiles:\n  - pattern: \"^\\\\.github/workflows/.*\\\\.ya?ml$\"\n\nignore_actions:\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - \"**\"\n  pull_request:\n    branches:\n      - \"**\"\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        redis: [6.2, 7.4, 8.0]\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Get Deno version\n        run: |\n          echo \"DENO_VERSION=$(cat .denov)\" >> $GITHUB_ENV\n      - name: Set up Deno ${{ env.DENO_VERSION }}\n        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4\n        with:\n          deno-version: ${{ env.DENO_VERSION }}\n          cache: true\n          cache-hash: ${{ hashFiles('.denov', 'deno.lock') }}\n      - name: Set up Redis ${{ matrix.redis }}\n        uses: shogo82148/actions-setup-redis@2f3253b148c73d7a0682eae73e862b777a4fa74e # v1.49.0\n        with:\n          redis-version: ${{ matrix.redis }}\n          auto-start: \"true\"\n      - name: Run tests\n        run: |\n          deno task test\n        env:\n          REDIS_VERSION: ${{ matrix.redis }}\n      - name: Run doc tests\n        run: |\n          deno task test:doc\n      - uses: k1LoW/octocov-action@73d561f65d59e66899ed5c87e4621a913b5d5c20 # v1.5.0\n        if: ${{ github.event_name == 'pull_request' && matrix.redis == 8 }}\n\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Get Deno version\n        run: |\n          echo \"DENO_VERSION=$(cat .denov)\" >> $GITHUB_ENV\n      - name: Set up Deno ${{ env.DENO_VERSION }}\n        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4\n        with:\n          deno-version: ${{ env.DENO_VERSION }}\n          cache: true\n          cache-hash: ${{ hashFiles('.denov', 'deno.lock') }}\n      - name: Run linters\n        run: |\n          deno task check\n      - name: deno publish --dry-run\n        run:\n          deno publish --dry-run\n\n  benchmark:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        redis: [8.0]\n        driver: [deno-redis, ioredis]\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Get Deno version\n        run: |\n          echo \"DENO_VERSION=$(cat .denov)\" >> $GITHUB_ENV\n      - name: Set up Deno ${{ env.DENO_VERSION }}\n        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4\n        with:\n          deno-version: ${{ env.DENO_VERSION }}\n          cache: true\n          cache-hash: ${{ hashFiles('.denov', 'deno.lock') }}\n      - name: Set up Node.js\n        if: matrix.driver == 'ioredis'\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: \"24\"\n      - uses: bahmutov/npm-install@20216767ca67f0f7b4d095dc5859c5700a6581cb # v1.12.1\n        if: matrix.driver == 'ioredis'\n        with:\n          working-directory: benchmark\n      - name: Set up Redis ${{ matrix.redis }}\n        uses: shogo82148/actions-setup-redis@2f3253b148c73d7a0682eae73e862b777a4fa74e # v1.49.0\n        with:\n          redis-version: ${{ matrix.redis }}\n          auto-start: \"true\"\n      - name: Run benchmarks\n        run: |\n          deno task bench:${{ matrix.driver }}\n      - name: Output results as a job summary\n        run: |\n          deno run --allow-read=tmp tools/format-benchmark-results.js >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to JSR\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Get Deno version\n        run: |\n          echo \"DENO_VERSION=$(cat .denov)\" >> $GITHUB_ENV\n      - name: Set up Deno ${{ env.DENO_VERSION }}\n        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4\n        with:\n          deno-version: ${{ env.DENO_VERSION }}\n      - name: Publish packages\n        run:\n          deno publish\n"
  },
  {
    "path": ".gitignore",
    "content": "deno.d.ts\n.idea\ntestdata/*/\nbenchmark/node_modules\ntests/tmp/*/\ncoverage\ntmp\n.vscode/settings.json\n"
  },
  {
    "path": ".octocov.yml",
    "content": "# generated by octocov init\ncodeToTestRatio:\n  code:\n    - \"**/*.ts\"\n    - \"!tests/*.ts\"\n  test:\n    - \"tests/*.ts\"\ncoverage:\n  paths:\n    - coverage/lcov.info\ntestExecutionTime:\n  if: true\ndiff:\n  datastores:\n    - artifact://${GITHUB_REPOSITORY}\ncomment:\n  # TODO: enable this\n  if: is_pull_request\n  # if: is_pull_request\nsummary:\n  if: true\nreport:\n  if: is_default_branch\n  datastores:\n    - artifact://${GITHUB_REPOSITORY}\nrepository: denodrivers/redis\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Yusuke Sakurai\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# deno-redis\n\n[![JSR](https://jsr.io/badges/@db/redis)](https://jsr.io/@db/redis)\n[![Build Status](https://github.com/denodrivers/redis/workflows/CI/badge.svg)](https://github.com/denodrivers/redis/actions)\n[![license](https://img.shields.io/github/license/denodrivers/redis.svg)](https://github.com/denodrivers/redis)\n[![Discord](https://img.shields.io/discord/768918486575480863?logo=discord)](https://discord.gg/QXuHBMcgWx)\n\nAn experimental implementation of redis client for deno\n\n## Usage\n\n### Installation\n\n```shell\n$ deno add jsr:@db/redis\n```\n\n### Permissions\n\n`deno-redis` needs `--allow-net` privilege\n\n### Stateless Commands\n\n```ts\nimport { connect } from \"@db/redis\";\nconst redis = await connect({\n  hostname: \"127.0.0.1\",\n  port: 6379,\n});\nconst ok = await redis.set(\"hoge\", \"fuga\");\nconst fuga = await redis.get(\"hoge\");\n```\n\n### Pub/Sub\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\nconst sub = await redis.subscribe(\"channel\");\n(async function () {\n  for await (const { channel, message } of sub.receive()) {\n    // on message\n  }\n})();\n```\n\n### Streams\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\nawait redis.xadd(\n  \"somestream\",\n  \"*\", // let redis assign message ID\n  { yes: \"please\", no: \"thankyou\" },\n  { elements: 10 },\n);\n\nconst [stream] = await redis.xread(\n  [{ key: \"somestream\", xid: 0 }], // read from beginning\n  { block: 5000 },\n);\n\nconst msgFV = stream.messages[0].fieldValues;\nconst plz = msgFV[\"yes\"];\nconst thx = msgFV[\"no\"];\n```\n\n### Cluster\n\nFirst, if you need to set up nodes into a working redis cluster:\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\", port: 6379 });\n\n// connect each node to form a cluster (see https://redis.io/commands/cluster-meet)\nawait redis.clusterMeet(\"127.0.0.1\", 6380);\n// ...\n\n// List the nodes in the cluster\nawait redis.clusterNodes();\n// ... 127.0.0.1:6379@16379 myself,master - 0 1593978765000 0 connected\n// ... 127.0.0.1:6380@16380 master - 0 1593978766503 1 connected\n```\n\nTo consume a redis cluster, you can use\n[experimental/cluster module](experimental/cluster/README.md).\n\n## Advanced Usage\n\n### Handle connection timeout\n\nConnection timeout handling can be implemented with `signal` option:\n\n```ts\nimport { connect } from \"@db/redis\";\n\nusing redis = await connect({\n  hostname: \"127.0.0.1\",\n  signal: () => AbortSignal.timeout(5_000),\n});\n```\n\n### Retriable connection\n\nBy default, a client's connection will retry a command execution based on\nexponential backoff algorithm if the server dies or the network becomes\nunavailable. You can change the maximum number of retries by setting\n`maxRetryCount` (It's default to `10`):\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\", maxRetryCount: 0 }); // Disable retries\n```\n\n### Execute raw commands\n\n`Redis.sendCommand` is low level interface for\n[redis protocol](https://redis.io/topics/protocol). You can send raw redis\ncommands and receive replies.\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\n\nconst reply = await redis.sendCommand(\"SET\", [\"redis\", \"nice\"]);\nconsole.assert(reply === \"OK\");\n```\n\nIf `returnUint8Arrays` option is set to `true`, simple strings and bulk strings\nare returned as `Uint8Array`\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\n\nconst reply = await redis.sendCommand(\"GET\", [\"redis\"], {\n  returnUint8Arrays: true,\n});\nconsole.assert(reply instanceof Uint8Array);\n```\n\n### Pipelining\n\nhttps://redis.io/topics/pipelining\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\nconst pl = redis.pipeline();\npl.ping();\npl.ping();\npl.set(\"set1\", \"value1\");\npl.set(\"set2\", \"value2\");\npl.mget(\"set1\", \"set2\");\npl.del(\"set1\");\npl.del(\"set2\");\nconst replies = await pl.flush();\n```\n\n### TxPipeline (pipeline with MULTI/EXEC)\n\nWe recommend to use `tx()` instead of `multi()/exec()` for transactional\noperation. `MULTI/EXEC` are potentially stateful operation so that operation's\natomicity is guaranteed but redis's state may change between MULTI and EXEC.\n\n`WATCH` is designed for these problems. You can ignore it by using TxPipeline\nbecause pipelined MULTI/EXEC commands are strictly executed in order at the time\nand no changes will happen during execution.\n\nSee detail https://redis.io/topics/transactions\n\n```ts\nimport { connect } from \"@db/redis\";\n\nconst redis = await connect({ hostname: \"127.0.0.1\" });\nconst tx = redis.tx();\ntx.set(\"a\", \"aa\");\ntx.set(\"b\", \"bb\");\ntx.del(\"c\");\nawait tx.flush();\n// MULTI\n// SET a aa\n// SET b bb\n// DEL c\n// EXEC\n```\n\n### Client side caching\n\nhttps://redis.io/topics/client-side-caching\n\n```typescript\nimport { connect } from \"@db/redis\";\n\nconst mainClient = await connect({ hostname: \"127.0.0.1\" });\nconst cacheClient = await connect({ hostname: \"127.0.0.1\" });\n\nconst cacheClientID = await cacheClient.clientID();\nawait mainClient.clientTracking({\n  mode: \"ON\",\n  redirect: cacheClientID,\n});\nconst sub = await cacheClient.subscribe<string[]>(\"__redis__:invalidate\");\n\n(async () => {\n  for await (const { channel, message } of sub.receive()) {\n    // Handle invalidation messages...\n  }\n})();\n```\n\n### Connection pooling\n\n> [!WARNING]\n> This feature is still experimental and may change in the future.\n\n`@db/redis/experimental/pool` module provides connection pooling:\n\n```typescript\nimport { createPoolClient } from \"@db/redis/experimental/pool\";\n\nconst redis = await createPoolClient({\n  connection: {\n    hostname: \"127.0.0.1\",\n    port: 6379,\n  },\n});\nawait redis.set(\"foo\", \"bar\");\nawait redis.get(\"foo\");\n```\n\n### Experimental features\n\ndeno-redis provides some experimental features.\n\nSee [experimental/README.md](experimental/README.md) for details.\n\n## Roadmap for v1\n\n- See https://github.com/denodrivers/redis/issues/78\n"
  },
  {
    "path": "backoff.ts",
    "content": "export interface Backoff {\n  /**\n   * Returns the next backoff interval in milliseconds\n   */\n  (attempts: number): number;\n}\n\nexport interface ExponentialBackoffOptions {\n  /**\n   * @default 2\n   */\n  multiplier: number;\n  /**\n   * The maximum backoff interval in milliseconds\n   * @default 5000\n   */\n  maxInterval: number;\n  /**\n   * The minimum backoff interval in milliseconds\n   * @default 500\n   */\n  minInterval: number;\n}\n\nexport function exponentialBackoff({\n  multiplier = 2,\n  maxInterval = 5000,\n  minInterval = 500,\n}: Partial<ExponentialBackoffOptions> = {}): Backoff {\n  return (attempts) =>\n    Math.min(maxInterval, minInterval * (multiplier ** (attempts - 1)));\n}\n"
  },
  {
    "path": "benchmark/.npmrc",
    "content": "min-release-age=1\nignore-scripts=true\n"
  },
  {
    "path": "benchmark/benchmark.js",
    "content": "import { add, complete, configure, cycle, save, suite } from \"benny\";\nimport { dirname, join } from \"node:path\";\n\nexport function run({\n  driver,\n  client,\n  outputFilename = driver,\n}) {\n  const encoder = new TextEncoder();\n  return suite(\n    driver,\n    configure({ minSamples: 10 }),\n    add(\"ping\", async () => {\n      await client.ping(\"HELLO\");\n    }),\n    add(\"set & get\", () => {\n      const value = \"bar\".repeat(10);\n      return async () => {\n        const key = \"foo\";\n        await client.set(key, value);\n        await client.get(key);\n      };\n    }),\n    add(\"set Uint8Array\", () => {\n      const value = encoder.encode(\"abcde\".repeat(100));\n      return async () => {\n        const key = \"bytes\";\n        await client.set(key, value);\n      };\n    }),\n    add(\"mset & mget\", async () => {\n      await client.mset({ a: \"foo\", b: encoder.encode(\"bar\") });\n      await client.mget(\"a\", \"b\");\n    }),\n    add(\"zadd & zscore\", async () => {\n      await client.zadd(\"zset\", 1234567, \"member\");\n      await client.zscore(\"zset\", \"member\");\n    }),\n    cycle(),\n    complete((summary) => {\n      const results = summary.results.map((result) => {\n        const {\n          name,\n          ops,\n          margin,\n          details: {\n            min,\n            max,\n            mean,\n            median,\n          },\n          samples,\n        } = result;\n        return {\n          name,\n          ops,\n          margin,\n          min,\n          max,\n          mean,\n          median,\n          samples,\n        };\n      });\n      console.table(results);\n    }),\n    complete(async () => {\n      await client.flushdb();\n    }),\n    save({\n      file: outputFilename,\n      details: true,\n      folder: join(\n        dirname(dirname(new URL(import.meta.url).pathname)),\n        \"tmp/benchmark\",\n      ),\n    }),\n  );\n}\n"
  },
  {
    "path": "benchmark/deno-redis.ts",
    "content": "import { run } from \"./benchmark.js\";\nimport { connect } from \"../mod.ts\";\nimport { connect as connectWebStreams } from \"../experimental/web_streams_connection/mod.ts\";\n\n{\n  const redis = await connect({\n    hostname: \"127.0.0.1\",\n    noDelay: true,\n  });\n  try {\n    await run({\n      client: redis,\n      driver: \"deno-redis\",\n    });\n  } finally {\n    await redis.quit();\n  }\n}\n\n{\n  const redis = await connectWebStreams({ hostname: \"127.0.0.1\" });\n  try {\n    await run({\n      client: redis,\n      driver: \"deno-redis (experimental/web_streams_connection)\",\n      outputFilename:\n        \"deno-redis-with-experimental-web-streams-based-connection\",\n    });\n  } finally {\n    await redis.quit();\n  }\n}\n"
  },
  {
    "path": "benchmark/ioredis.js",
    "content": "import { run } from \"./benchmark.js\";\nimport Redis from \"ioredis\";\n\nconst redis = new Redis();\nredis.on(\"connect\", async () => {\n  await run({\n    client: redis,\n    driver: \"ioredis\",\n  });\n\n  redis.disconnect();\n});\n"
  },
  {
    "path": "benchmark/package.json",
    "content": "{\n  \"name\": \"deno-redis-benchmark\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"devDependencies\": {\n    \"benny\": \"^3.7.1\",\n    \"ioredis\": \"^4.22.0\"\n  }\n}\n"
  },
  {
    "path": "client.ts",
    "content": "import type { Connection, SendCommandOptions } from \"./connection.ts\";\nimport type { RedisReply, RedisValue } from \"./protocol/shared/types.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisSubscription,\n  SubscribeCommand,\n} from \"./subscription.ts\";\n\n/**\n * A low-level client for Redis.\n */\nexport interface Client {\n  /**\n   * @deprecated\n   */\n  readonly connection: Connection;\n  /**\n   * @deprecated\n   */\n  exec(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<RedisReply>;\n\n  sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ): Promise<RedisReply>;\n\n  subscribe<TMessage extends PubSubMessageType = DefaultPubSubMessageType>(\n    command: SubscribeCommand,\n    ...channelsOrPatterns: Array<string>\n  ): Promise<RedisSubscription<TMessage>>;\n\n  /**\n   * Closes a redis connection.\n   */\n  close(): void;\n}\n"
  },
  {
    "path": "command.ts",
    "content": "import type {\n  Binary,\n  Bulk,\n  BulkNil,\n  BulkString,\n  ConditionalArray,\n  Integer,\n  Protover,\n  Raw,\n  RedisValue,\n  SimpleString,\n} from \"./protocol/shared/types.ts\";\nimport type { RedisPipeline } from \"./pipeline.ts\";\nimport type { RedisSubscription } from \"./subscription.ts\";\nimport type {\n  StartEndCount,\n  XAddFieldValues,\n  XClaimOpts,\n  XClaimReply,\n  XId,\n  XIdAdd,\n  XIdInput,\n  XIdNeg,\n  XIdPos,\n  XInfoConsumersReply,\n  XInfoGroupsReply,\n  XInfoStreamFullReply,\n  XInfoStreamReply,\n  XKeyId,\n  XKeyIdGroup,\n  XKeyIdGroupLike,\n  XKeyIdLike,\n  XMaxlen,\n  XMessage,\n  XPendingCount,\n  XPendingReply,\n  XReadGroupOpts,\n  XReadOpts,\n  XReadReply,\n} from \"./stream.ts\";\n\nexport type ACLLogMode = \"RESET\";\ntype BitopOperation = \"AND\" | \"OR\" | \"XOR\" | \"NOT\";\n\nexport interface BitfieldOpts {\n  get?: { type: string; offset: number | string };\n  set?: { type: string; offset: number | string; value: number };\n  incrby?: { type: string; offset: number | string; increment: number };\n}\n\nexport interface BitfieldWithOverflowOpts extends BitfieldOpts {\n  overflow: \"WRAP\" | \"SAT\" | \"FAIL\";\n}\n\nexport type ClientCachingMode = \"YES\" | \"NO\";\n\nexport interface ClientKillOpts {\n  addr?: string; // ip:port\n  laddr?: string; // ip:port\n  id?: number;\n  type?: ClientType;\n  user?: string;\n  skipme?: \"YES\" | \"NO\";\n}\n\nexport interface ClientListOpts {\n  type?: ClientType;\n  ids?: number[];\n}\n\nexport type ClientPauseMode = \"WRITE\" | \"ALL\";\n\nexport interface ClientTrackingOpts {\n  mode: \"ON\" | \"OFF\";\n  redirect?: number;\n  prefixes?: string[];\n  bcast?: boolean;\n  optIn?: boolean;\n  optOut?: boolean;\n  noLoop?: boolean;\n}\n\nexport type ClientType = \"NORMAL\" | \"MASTER\" | \"REPLICA\" | \"PUBSUB\";\nexport type ClientUnblockingBehaviour = \"TIMEOUT\" | \"ERROR\";\n\nexport type ClusterFailoverMode = \"FORCE\" | \"TAKEOVER\";\nexport type ClusterResetMode = \"HARD\" | \"SOFT\";\nexport type ClusterSetSlotSubcommand =\n  | \"IMPORTING\"\n  | \"MIGRATING\"\n  | \"NODE\"\n  | \"STABLE\";\n\nexport interface MigrateOpts {\n  copy?: boolean;\n  replace?: boolean;\n  auth?: string;\n  keys?: string[];\n}\n\nexport interface RestoreOpts {\n  replace?: boolean;\n  absttl?: boolean;\n  idletime?: number;\n  freq?: number;\n}\n\nexport interface HelloOpts {\n  protover: Protover;\n  auth?: {\n    username: string;\n    password: string;\n  };\n  clientName?: string;\n}\n\nexport interface StralgoOpts {\n  idx?: boolean;\n  len?: boolean;\n  minmatchlen?: number;\n  withmatchlen?: boolean;\n}\n\nexport type StralgoAlgorithm = \"LCS\";\nexport type StralgoTarget = \"KEYS\" | \"STRINGS\";\n\nexport interface SetOpts {\n  ex?: number;\n  px?: number;\n  /** Sets `NX` option. */\n  nx?: boolean;\n  /** Sets `XX` option. */\n  xx?: boolean;\n  /**\n   * Sets `EXAT` option.\n   *\n   * `EXAT` option was added in Redis v6.2.\n   */\n  exat?: number;\n  /**\n   * Sets `PXAT` option.\n   *\n   * `PXAT` option was added in Redis v6.2.\n   */\n  pxat?: number;\n  keepttl?: boolean;\n  /**\n   * Enables `GET` option.\n   *\n   * `GET` option was added in Redis v6.2.\n   */\n  get?: boolean;\n}\n/** Return type for {@linkcode RedisCommands.set} */\nexport type SetReply<T extends SetOpts | SetWithModeOpts> = T extends\n  { get: true } ? SimpleString | BulkNil\n  : T extends { nx: true } ? SimpleString | BulkNil\n  : T extends { xx: true } ? SimpleString | BulkNil\n  : T extends SetWithModeOpts ? SimpleString | BulkNil\n  : SimpleString;\n\n/**\n * @deprecated Use {@linkcode SetOpts.nx}/{@linkcode SetOpts.xx} instead. This type will be removed in the future.\n */\nexport interface SetWithModeOpts extends SetOpts {\n  /**\n   * @deprecated Use {@linkcode SetOpts.nx}/{@linkcode SetOpts.xx} instead. This option will be removed in the future.\n   */\n  mode: \"NX\" | \"XX\";\n}\n\nexport interface GeoRadiusOpts {\n  withCoord?: boolean;\n  withDist?: boolean;\n  withHash?: boolean;\n  count?: number;\n  sort?: \"ASC\" | \"DESC\";\n  store?: string;\n  storeDist?: string;\n}\n\nexport type GeoUnit = \"m\" | \"km\" | \"ft\" | \"mi\";\n\ninterface BaseScanOpts {\n  pattern?: string;\n  count?: number;\n}\n\nexport interface ScanOpts extends BaseScanOpts {\n  type?: string;\n}\n\nexport type HScanOpts = BaseScanOpts;\nexport type SScanOpts = BaseScanOpts;\nexport type ZScanOpts = BaseScanOpts;\n\nexport interface ZAddOpts {\n  /** @deprecated Use {@linkcode ZAddOpts.nx}/{@linkcode ZAddOpts.xx} instead. This option will be removed in the future. */\n  mode?: \"NX\" | \"XX\";\n  ch?: boolean;\n  /** Enables `NX` option */\n  nx?: boolean;\n  /** Enables `XX` option */\n  xx?: boolean;\n}\n/** Return type for {@linkcode RedisCommands.zadd} */\nexport type ZAddReply<T extends ZAddOpts> =\n  //  TODO: Uncomment the following:\n  //  T extends { mode: \"NX\" | \"XX\" }\n  //  ? Integer | BulkNil\n  //  :\n  T extends { nx: true } ? Integer | BulkNil\n    : T extends { xx: true } ? Integer | BulkNil\n    : Integer;\n\ninterface ZStoreOpts {\n  aggregate?: \"SUM\" | \"MIN\" | \"MAX\";\n}\n\nexport type ZInterstoreOpts = ZStoreOpts;\nexport type ZUnionstoreOpts = ZStoreOpts;\n\nexport interface ZRangeOpts {\n  withScore?: boolean;\n}\n\nexport type ZInterOpts = {\n  withScore?: boolean;\n} & ZStoreOpts;\n\nexport interface ZRangeByLexOpts {\n  limit?: { offset: number; count: number };\n}\n\nexport interface ZRangeByScoreOpts {\n  withScore?: boolean;\n  limit?: { offset: number; count: number };\n}\n\ninterface BaseLPosOpts {\n  rank?: number;\n  maxlen?: number;\n}\n\nexport interface LPosOpts extends BaseLPosOpts {\n  count?: null | undefined;\n}\n\nexport interface LPosWithCountOpts extends BaseLPosOpts {\n  count: number;\n}\n\nexport type LInsertLocation = \"BEFORE\" | \"AFTER\";\n\nexport interface MemoryUsageOpts {\n  samples?: number;\n}\n\ntype RoleReply =\n  | [\"master\", Integer, BulkString[][]]\n  | [\"slave\", BulkString, Integer, BulkString, Integer]\n  | [\"sentinel\", BulkString[]];\n\nexport type ScriptDebugMode = \"YES\" | \"SYNC\" | \"NO\";\n\nexport interface SortOpts {\n  by?: string;\n  limit?: { offset: number; count: number };\n  patterns?: string[];\n  order?: \"ASC\" | \"DESC\";\n  alpha?: boolean;\n}\n\nexport interface SortWithDestinationOpts extends SortOpts {\n  destination: string;\n}\n\nexport type ShutdownMode = \"NOSAVE\" | \"SAVE\";\n\nexport interface RedisCommands {\n  // Connection\n  auth(password: string): Promise<SimpleString>;\n  auth(username: string, password: string): Promise<SimpleString>;\n  echo(message: RedisValue): Promise<BulkString>;\n  ping(): Promise<SimpleString>;\n  ping(message: RedisValue): Promise<BulkString>;\n  quit(): Promise<SimpleString>;\n  select(index: number): Promise<SimpleString>;\n  hello(opts?: HelloOpts): Promise<ConditionalArray>;\n\n  // Keys\n  del(...keys: string[]): Promise<Integer>;\n  dump(key: string): Promise<Binary | BulkNil>;\n  exists(...keys: string[]): Promise<Integer>;\n  expire(key: string, seconds: number): Promise<Integer>;\n  expireat(key: string, timestamp: string): Promise<Integer>;\n  keys(pattern: string): Promise<BulkString[]>;\n  migrate(\n    host: string,\n    port: number | string,\n    key: string,\n    destination_db: string,\n    timeout: number,\n    opts?: MigrateOpts,\n  ): Promise<SimpleString>;\n  move(key: string, db: string): Promise<Integer>;\n  objectRefCount(key: string): Promise<Integer | BulkNil>;\n  objectEncoding(key: string): Promise<Bulk>;\n  objectIdletime(key: string): Promise<Integer | BulkNil>;\n  objectFreq(key: string): Promise<Integer | BulkNil>;\n  objectHelp(): Promise<BulkString[]>;\n  persist(key: string): Promise<Integer>;\n  pexpire(key: string, milliseconds: number): Promise<Integer>;\n  pexpireat(key: string, milliseconds_timestamp: number): Promise<Integer>;\n  pttl(key: string): Promise<Integer>;\n  randomkey(): Promise<Bulk>;\n  rename(key: string, newkey: string): Promise<SimpleString>;\n  renamenx(key: string, newkey: string): Promise<Integer>;\n  restore(\n    key: string,\n    ttl: number,\n    serialized_value: Binary,\n    opts?: RestoreOpts,\n  ): Promise<SimpleString>;\n  scan(\n    cursor: number,\n    opts?: ScanOpts,\n  ): Promise<[BulkString, BulkString[]]>;\n  sort(\n    key: string,\n    opts?: SortOpts,\n  ): Promise<BulkString[]>;\n  sort(\n    key: string,\n    opts?: SortWithDestinationOpts,\n  ): Promise<Integer>;\n  touch(...keys: string[]): Promise<Integer>;\n  ttl(key: string): Promise<Integer>;\n  type(key: string): Promise<SimpleString>;\n  unlink(...keys: string[]): Promise<Integer>;\n  wait(numreplicas: number, timeout: number): Promise<Integer>;\n\n  // String\n  append(key: string, value: RedisValue): Promise<Integer>;\n  bitcount(key: string): Promise<Integer>;\n  bitcount(key: string, start: number, end: number): Promise<Integer>;\n  bitfield(\n    key: string,\n    opts?: BitfieldOpts,\n  ): Promise<Integer[]>;\n  bitfield(\n    key: string,\n    opts?: BitfieldWithOverflowOpts,\n  ): Promise<(Integer | BulkNil)[]>;\n  bitop(\n    operation: BitopOperation,\n    destkey: string,\n    ...keys: string[]\n  ): Promise<Integer>;\n  bitpos(\n    key: string,\n    bit: number,\n    start?: number,\n    end?: number,\n  ): Promise<Integer>;\n  decr(key: string): Promise<Integer>;\n  decrby(key: string, decrement: number): Promise<Integer>;\n  get(key: string): Promise<Bulk>;\n  getbit(key: string, offset: number): Promise<Integer>;\n  getrange(key: string, start: number, end: number): Promise<BulkString>;\n  getset(key: string, value: RedisValue): Promise<Bulk>;\n  incr(key: string): Promise<Integer>;\n  incrby(key: string, increment: number): Promise<Integer>;\n  incrbyfloat(key: string, increment: number): Promise<BulkString>;\n  mget(...keys: string[]): Promise<Bulk[]>;\n  mset(key: string, value: RedisValue): Promise<SimpleString>;\n  mset(...key_values: [string, RedisValue][]): Promise<SimpleString>;\n  mset(key_values: Record<string, RedisValue>): Promise<SimpleString>;\n  msetnx(key: string, value: RedisValue): Promise<Integer>;\n  msetnx(...key_values: [string, RedisValue][]): Promise<Integer>;\n  msetnx(key_values: Record<string, RedisValue>): Promise<Integer>;\n  psetex(\n    key: string,\n    milliseconds: number,\n    value: RedisValue,\n  ): Promise<SimpleString>;\n  set<TSetOpts extends SetOpts | SetWithModeOpts = SetOpts>(\n    key: string,\n    value: RedisValue,\n    opts?: TSetOpts,\n  ): Promise<SetReply<TSetOpts>>;\n  setbit(key: string, offset: number, value: RedisValue): Promise<Integer>;\n  setex(key: string, seconds: number, value: RedisValue): Promise<SimpleString>;\n  setnx(key: string, value: RedisValue): Promise<Integer>;\n  setrange(key: string, offset: number, value: RedisValue): Promise<Integer>;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n  ): Promise<Bulk>;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { len: true },\n  ): Promise<Integer>;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { idx: true },\n  ): Promise<\n    [\n      string, //`\"matches\"`\n      Array<[[number, number], [number, number]]>,\n      string, // `\"len\"`\n      Integer,\n    ]\n  >;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { idx: true; withmatchlen: true },\n  ): Promise<\n    [\n      string, // `\"matches\"`\n      Array<[[number, number], [number, number], number]>,\n      string, // `\"len\"`\n      Integer,\n    ]\n  >;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: StralgoOpts,\n  ): Promise<\n    Bulk | Integer | [\n      string, // `\"matches\"`\n      Array<[[number, number], [number, number], number | undefined]>,\n      string, // `\"len\"`\n      Integer,\n    ]\n  >;\n\n  strlen(key: string): Promise<Integer>;\n\n  // Geo\n  geoadd(\n    key: string,\n    longitude: number,\n    latitude: number,\n    member: string,\n  ): Promise<Integer>;\n  geoadd(\n    key: string,\n    ...lng_lat_members: [number, number, string][]\n  ): Promise<Integer>;\n  geoadd(\n    key: string,\n    member_lng_lats: Record<string, [number, number]>,\n  ): Promise<Integer>;\n  geohash(key: string, ...members: string[]): Promise<Bulk[]>;\n  geopos(\n    key: string,\n    ...members: string[]\n  ): Promise<Array<[BulkString, BulkString] | BulkNil | []> | BulkNil>;\n  geodist(\n    key: string,\n    member1: string,\n    member2: string,\n    unit?: \"m\" | \"km\" | \"ft\" | \"mi\",\n  ): Promise<Bulk>;\n  // FIXME: Return type is too conditional\n  georadius(\n    key: string,\n    longitude: number,\n    latitude: number,\n    radius: number,\n    unit: GeoUnit,\n    opts?: GeoRadiusOpts,\n  ): Promise<ConditionalArray>;\n  // FIXME: Return type is too conditional\n  georadiusbymember(\n    key: string,\n    member: string,\n    radius: number,\n    unit: GeoUnit,\n    opts?: GeoRadiusOpts,\n  ): Promise<ConditionalArray>;\n\n  // Hash\n  hdel(key: string, ...fields: string[]): Promise<Integer>;\n  hexists(key: string, field: string): Promise<Integer>;\n  hget(key: string, field: string): Promise<Bulk>;\n  hgetall(key: string): Promise<BulkString[]>;\n  hincrby(key: string, field: string, increment: number): Promise<Integer>;\n  hincrbyfloat(\n    key: string,\n    field: string,\n    increment: number,\n  ): Promise<BulkString>;\n  hkeys(key: string): Promise<BulkString[]>;\n  hlen(key: string): Promise<Integer>;\n  hmget(key: string, ...fields: string[]): Promise<Bulk[]>;\n  /**\n   * @deprecated since 4.0.0, use hset\n   */\n  hmset(key: string, field: string, value: RedisValue): Promise<SimpleString>;\n  /**\n   * @deprecated since 4.0.0, use hset\n   */\n  hmset(\n    key: string,\n    ...field_values: [string, RedisValue][]\n  ): Promise<SimpleString>;\n  /**\n   * @deprecated since 4.0.0, use hset\n   */\n  hmset(\n    key: string,\n    field_values: Record<string, RedisValue>,\n  ): Promise<SimpleString>;\n  hscan(\n    key: string,\n    cursor: number,\n    opts?: HScanOpts,\n  ): Promise<[BulkString, BulkString[]]>;\n\n  /**\n   * @description Sets `field` in the hash to `value`.\n   * @see https://redis.io/commands/hset\n   */\n  hset(key: string, field: string, value: RedisValue): Promise<Integer>;\n\n  /**\n   * @description Sets the field-value pairs specified by `fieldValues` to the hash stored at `key`.\n   *   NOTE: Variadic form for `HSET` is supported only in Redis v4.0.0 or higher.\n   */\n  hset(key: string, ...fieldValues: [string, RedisValue][]): Promise<Integer>;\n\n  /**\n   * @description Sets the field-value pairs specified by `fieldValues` to the hash stored at `key`.\n   *   NOTE: Variadic form for `HSET` is supported only in Redis v4.0.0 or higher.\n   */\n  hset(key: string, fieldValues: Record<string, RedisValue>): Promise<Integer>;\n  hsetnx(key: string, field: string, value: RedisValue): Promise<Integer>;\n  hstrlen(key: string, field: string): Promise<Integer>;\n  hvals(key: string): Promise<BulkString[]>;\n\n  // List\n  blpop(\n    timeout: number,\n    ...keys: string[]\n  ): Promise<[BulkString, BulkString] | BulkNil>;\n  brpop(\n    timeout: number,\n    ...keys: string[]\n  ): Promise<[BulkString, BulkString] | BulkNil>;\n  brpoplpush(\n    source: string,\n    destination: string,\n    timeout: number,\n  ): Promise<Bulk>;\n  lindex(key: string, index: number): Promise<Bulk>;\n  linsert(\n    key: string,\n    loc: LInsertLocation,\n    pivot: string,\n    value: RedisValue,\n  ): Promise<Integer>;\n  llen(key: string): Promise<Integer>;\n  lpop(key: string): Promise<Bulk>;\n  lpop(key: string, count: number): Promise<Array<BulkString>>;\n\n  /**\n   * Returns the index of the first matching element inside a list.\n   * If no match is found, this method returns `undefined`.\n   */\n  lpos(\n    key: string,\n    element: RedisValue,\n    opts?: LPosOpts,\n  ): Promise<Integer | BulkNil>;\n\n  /**\n   * Returns the indexes of the first N matching elements inside a list.\n   * If no match is found. this method returns an empty array.\n   *\n   * @param opts.count Maximum length of the indices returned by this method\n   */\n  lpos(\n    key: string,\n    element: RedisValue,\n    opts: LPosWithCountOpts,\n  ): Promise<Integer[]>;\n\n  lpush(key: string, ...elements: RedisValue[]): Promise<Integer>;\n  lpushx(key: string, ...elements: RedisValue[]): Promise<Integer>;\n  lrange(key: string, start: number, stop: number): Promise<BulkString[]>;\n  lrem(key: string, count: number, element: RedisValue): Promise<Integer>;\n  lset(key: string, index: number, element: RedisValue): Promise<SimpleString>;\n  ltrim(key: string, start: number, stop: number): Promise<SimpleString>;\n  rpop(key: string): Promise<Bulk>;\n  rpoplpush(source: string, destination: string): Promise<Bulk>;\n  rpush(key: string, ...elements: RedisValue[]): Promise<Integer>;\n  rpushx(key: string, ...elements: RedisValue[]): Promise<Integer>;\n\n  // HyperLogLog\n  pfadd(key: string, ...elements: string[]): Promise<Integer>;\n  pfcount(...keys: string[]): Promise<Integer>;\n  pfmerge(destkey: string, ...sourcekeys: string[]): Promise<SimpleString>;\n\n  // PubSub\n  psubscribe<TMessage extends string | string[] = string>(\n    ...patterns: string[]\n  ): Promise<RedisSubscription<TMessage>>;\n  pubsubChannels(pattern?: string): Promise<BulkString[]>;\n  pubsubNumsub(...channels: string[]): Promise<(BulkString | Integer)[]>;\n  pubsubNumpat(): Promise<Integer>;\n  publish(channel: string, message: RedisValue): Promise<Integer>;\n  subscribe<TMessage extends string | string[] = string>(\n    ...channels: string[]\n  ): Promise<RedisSubscription<TMessage>>;\n\n  // Set\n  sadd(key: string, ...members: RedisValue[]): Promise<Integer>;\n  scard(key: string): Promise<Integer>;\n  sdiff(...keys: string[]): Promise<BulkString[]>;\n  sdiffstore(destination: string, ...keys: string[]): Promise<Integer>;\n  sinter(...keys: string[]): Promise<BulkString[]>;\n  sinterstore(destination: string, ...keys: string[]): Promise<Integer>;\n  sismember(key: string, member: RedisValue): Promise<Integer>;\n  smembers(key: string): Promise<BulkString[]>;\n  smove(\n    source: string,\n    destination: string,\n    member: RedisValue,\n  ): Promise<Integer>;\n  spop(key: string): Promise<Bulk>;\n  spop(key: string, count: number): Promise<BulkString[]>;\n  srandmember(key: string): Promise<Bulk>;\n  srandmember(key: string, count: number): Promise<BulkString[]>;\n  srem(key: string, ...members: RedisValue[]): Promise<Integer>;\n  sscan(\n    key: string,\n    cursor: number,\n    opts?: SScanOpts,\n  ): Promise<[BulkString, BulkString[]]>;\n  sunion(...keys: string[]): Promise<BulkString[]>;\n  sunionstore(destination: string, ...keys: string[]): Promise<Integer>;\n\n  // Stream\n  /**\n   * The XACK command removes one or multiple messages\n   * from the pending entries list (PEL) of a stream\n   *  consumer group. A message is pending, and as such\n   *  stored inside the PEL, when it was delivered to\n   * some consumer, normally as a side effect of calling\n   *  XREADGROUP, or when a consumer took ownership of a\n   *  message calling XCLAIM. The pending message was\n   * delivered to some consumer but the server is yet not\n   *  sure it was processed at least once. So new calls\n   *  to XREADGROUP to grab the messages history for a\n   * consumer (for instance using an XId of 0), will\n   * return such message. Similarly the pending message\n   * will be listed by the XPENDING command, that\n   * inspects the PEL.\n   *\n   * Once a consumer successfully processes a message,\n   * it should call XACK so that such message does not\n   * get processed again, and as a side effect, the PEL\n   * entry about this message is also purged, releasing\n   * memory from the Redis server.\n   *\n   * @param key the stream key\n   * @param group the group name\n   * @param xids the ids to acknowledge\n   */\n  xack(key: string, group: string, ...xids: XIdInput[]): Promise<Integer>;\n  /**\n   * Write a message to a stream.\n   *\n   * Returns bulk string reply, specifically:\n   * The command returns the XId of the added entry.\n   * The XId is the one auto-generated if * is passed\n   * as XId argument, otherwise the command just returns\n   *  the same XId specified by the user during insertion.\n   * @param key  write to this stream\n   * @param xid the XId of the entity written to the stream\n   * @param field_values  record object or map of field value pairs\n   */\n  xadd(\n    key: string,\n    xid: XIdAdd,\n    field_values: XAddFieldValues,\n  ): Promise<XId>;\n  /**\n   * Write a message to a stream.\n   *\n   * Returns bulk string reply, specifically:\n   * The command returns the XId of the added entry.\n   * The XId is the one auto-generated if * is passed\n   * as XId argument, otherwise the command just returns\n   *  the same XId specified by the user during insertion.\n   * @param key  write to this stream\n   * @param xid the XId of the entity written to the stream\n   * @param field_values  record object or map of field value pairs\n   * @param maxlen  number of elements, and whether or not to use an approximate comparison\n   */\n  xadd(\n    key: string,\n    xid: XIdAdd,\n    field_values: XAddFieldValues,\n    maxlen: XMaxlen,\n  ): Promise<XId>;\n  /**\n   * In the context of a stream consumer group, this command changes the ownership of a pending message, so that the new owner is the\n   * consumer specified as the command argument.\n   *\n   * It returns the claimed messages unless called with the JUSTIDs\n   * option, in which case it returns only their XIds.\n   *\n   * This is a complex command!  Read more at https://redis.io/commands/xclaim\n   *\n<pre>\nXCLAIM mystream mygroup Alice 3600000 1526569498055-0\n1) 1) 1526569498055-0\n   2) 1) \"message\"\n      2) \"orange\"\n</pre>\n\n   * @param key the stream name\n   * @param opts Various arguments for the command.  The following are required:\n   *    GROUP: the name of the consumer group which will claim the messages\n   *    CONSUMER: the specific consumer which will claim the message\n   *    MIN-IDLE-TIME:  claim messages whose idle time is greater than this number (milliseconds)\n   *\n   * The command has multiple options which can be omitted, however\n   * most are mainly for internal use in order to transfer the\n   * effects of XCLAIM or other commands to the AOF file and to\n   * propagate the same effects to the slaves, and are unlikely to\n   * be useful to normal users:\n   *    IDLE <ms>: Set the idle time (last time it was delivered) of the message. If IDLE is not specified, an IDLE of 0 is assumed, that is, the time count is reset because the message has now a new owner trying to process it.\n   *    TIME <ms-unix-time>: This is the same as IDLE but instead of a relative amount of milliseconds, it sets the idle time to a specific Unix time (in milliseconds). This is useful in order to rewrite the AOF file generating XCLAIM commands.\n   *    RETRYCOUNT <count>: Set the retry counter to the specified value. This counter is incremented every time a message is delivered again. Normally XCLAIM does not alter this counter, which is just served to clients when the XPENDING command is called: this way clients can detect anomalies, like messages that are never processed for some reason after a big number of delivery attempts.\n   *    FORCE: Creates the pending message entry in the PEL even if certain specified XIds are not already in the PEL assigned to a different client. However the message must be exist in the stream, otherwise the XIds of non existing messages are ignored.\n   *    JUSTXID: Return just an array of XIds of messages successfully claimed, without returning the actual message. Using this option means the retry counter is not incremented.\n   * @param xids the message XIds to claim\n   */\n  xclaim(\n    key: string,\n    opts: XClaimOpts,\n    ...xids: XIdInput[]\n  ): Promise<XClaimReply>;\n  /**\n   * Removes the specified entries from a stream,\n   * and returns the number of entries deleted,\n   * that may be different from the number of\n   * XIds passed to the command in case certain\n   * XIds do not exist.\n   *\n   * @param key the stream key\n   * @param xids ids to delete\n   */\n  xdel(key: string, ...xids: XIdInput[]): Promise<Integer>;\n  /**\n   * This command is used to create a new consumer group associated\n   * with a stream.\n   *\n   * <pre>\n   XGROUP CREATE test-man-000 test-group $ MKSTREAM\n   OK\n   </pre>\n   *\n   * See https://redis.io/commands/xgroup\n   * @param key stream key\n   * @param groupName the name of the consumer group\n   * @param xid The last argument is the XId of the last\n   *            item in the stream to consider already\n   *            delivered. In the above case we used the\n   *            special XId '$' (that means: the XId of the\n   *            last item in the stream). In this case\n   *            the consumers fetching data from that\n   *            consumer group will only see new elements\n   *            arriving in the stream.  If instead you\n   *            want consumers to fetch the whole stream\n   *            history, use zero as the starting XId for\n   *            the consumer group\n   * @param mkstream You can use the optional MKSTREAM subcommand as the last argument after the XId to automatically create the stream, if it doesn't exist. Note that if the stream is created in this way it will have a length of 0.\n   */\n  xgroupCreate(\n    key: string,\n    groupName: string,\n    xid: XIdInput | \"$\",\n    mkstream?: boolean,\n  ): Promise<SimpleString>;\n  /**\n   * Delete a specific consumer from a group, leaving\n   * the group itself intact.\n   *\n   * <pre>\nXGROUP DELCONSUMER test-man-000 hellogroup 4\n(integer) 0\n</pre>\n   * @param key stream key\n   * @param groupName the name of the consumer group\n   * @param consumerName the specific consumer to delete\n   */\n  xgroupDelConsumer(\n    key: string,\n    groupName: string,\n    consumerName: string,\n  ): Promise<Integer>;\n  /**\n   * Destroy a consumer group completely.  The consumer\n   * group will be destroyed even if there are active\n   * consumers and pending messages, so make sure to\n   * call this command only when really needed.\n   *\n<pre>\nXGROUP DESTROY test-man-000 test-group\n(integer) 1\n</pre>\n   * @param key stream key\n   * @param groupName the consumer group to destroy\n   */\n  xgroupDestroy(key: string, groupName: string): Promise<Integer>;\n  /** A support command which displays text about the\n   * various subcommands in XGROUP. */\n  xgroupHelp(): Promise<BulkString>;\n  /**\n     * Finally it possible to set the next message to deliver\n     * using the SETID subcommand. Normally the next XId is set\n     * when the consumer is created, as the last argument of\n     * XGROUP CREATE. However using this form the next XId can\n     * be modified later without deleting and creating the\n     * consumer group again. For instance if you want the\n     * consumers in a consumer group to re-process all the\n     * messages in a stream, you may want to set its next ID\n     * to 0:\n<pre>\nXGROUP SETID mystream consumer-group-name 0\n</pre>\n     *\n     * @param key  stream key\n     * @param groupName   the consumer group\n     * @param xid the XId to use for the next message delivered\n     */\n  xgroupSetID(\n    key: string,\n    groupName: string,\n    xid: XIdInput,\n  ): Promise<SimpleString>;\n  xinfoStream(key: string): Promise<XInfoStreamReply>;\n  /**\n   *  returns the entire state of the stream, including entries, groups, consumers and PELs. This form is available since Redis 6.0.\n   * @param key The stream key\n   */\n  xinfoStreamFull(key: string, count?: number): Promise<XInfoStreamFullReply>;\n  /**\n   * Get as output all the consumer groups associated\n   * with the stream.\n   *\n   * @param key the stream key\n   */\n  xinfoGroups(key: string): Promise<XInfoGroupsReply>;\n  /**\n   * Get the list of every consumer in a specific\n   * consumer group.\n   *\n   * @param key the stream key\n   * @param group list consumers for this group\n   */\n  xinfoConsumers(key: string, group: string): Promise<XInfoConsumersReply>;\n  /**\n   * Returns the number of entries inside a stream. If the specified key does not exist the command returns zero, as if the stream was empty. However note that unlike other Redis types, zero-length streams are possible, so you should call TYPE or EXISTS in order to check if a key exists or not.\n   * @param key  the stream key to inspect\n   */\n  xlen(key: string): Promise<Integer>;\n  /**\n   * Complex command to obtain info on messages in the Pending Entries List.\n   *\n   * Outputs a summary about the pending messages in a given consumer group.\n   *\n   * @param key get pending messages on this stream key\n   * @param group get pending messages for this group\n   */\n  xpending(\n    key: string,\n    group: string,\n  ): Promise<XPendingReply>;\n  /**\n   * Output more detailed info about pending messages:\n   *\n   *    - The ID of the message.\n   *    - The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message.\n   *    - The number of milliseconds that elapsed since the last time this message was delivered to this consumer.\n   *    - The number of times this message was delivered.\n   *\n   * If you pass the consumer argument to the command, it will efficiently filter for messages owned by that consumer.\n   * @param key get pending messages on this stream key\n   * @param group get pending messages for this group\n   * @param startEndCount start and end: XId range params. you may specify \"-\" for start and \"+\" for end. you must also provide a max count of messages.\n   * @param consumer optional, filter by this consumer as owner\n   */\n  xpendingCount(\n    key: string,\n    group: string,\n    startEndCount: StartEndCount,\n    consumer?: string,\n  ): Promise<XPendingCount[]>;\n  /**\n   * The command returns the stream entries matching a given\n   * range of XIds. The range is specified by a minimum and\n   * maximum ID. All the entries having an XId between the\n   * two specified or exactly one of the two XIds specified\n   * (closed interval) are returned.\n   *\n   * The command also has a reciprocal command returning\n   * items in the reverse order, called XREVRANGE, which\n   * is otherwise identical.\n   *\n   * The - and + special XIds mean respectively the minimum\n   * XId possible and the maximum XId possible inside a stream,\n   * so the following command will just return every\n   * entry in the stream.\n\n<pre>\nXRANGE somestream - +\n</pre>\n   * @param key  stream key\n   * @param start beginning XId, or -\n   * @param end  final XId, or +\n   * @param count max number of entries to return\n   */\n  xrange(\n    key: string,\n    start: XIdNeg,\n    end: XIdPos,\n    count?: number,\n  ): Promise<XMessage[]>;\n  /**\n   * This command is exactly like XRANGE, but with the\n   * notable difference of returning the entries in\n   * reverse order, and also taking the start-end range\n   * in reverse order: in XREVRANGE you need to state the\n   *  end XId and later the start ID, and the command will\n   *  produce all the element between (or exactly like)\n   * the two XIds, starting from the end side.\n   *\n   * @param key  the stream key\n   * @param start   reading backwards, start from this XId.  for the maximum, specify \"+\"\n   * @param end  stop at this XId.  for the minimum, specify \"-\"\n   * @param count max number of entries to return\n   */\n  xrevrange(\n    key: string,\n    start: XIdPos,\n    end: XIdNeg,\n    count?: number,\n  ): Promise<XMessage[]>;\n  /**\n   * Read data from one or multiple streams, only returning\n   * entries with an XId greater than the last received XId\n   * reported by the caller.\n   * @param key_xids pairs containing the stream key, and\n   *                    the XId from which to read\n   * @param opts optional max count of entries to return\n   *                    for each stream, and number of\n   *                    milliseconds for which to block\n   */\n  xread(\n    key_xids: (XKeyId | XKeyIdLike)[],\n    opts?: XReadOpts,\n  ): Promise<XReadReply>;\n  /**\n   * The XREADGROUP command is a special version of the XREAD command with support for consumer groups.\n   *\n   * @param key_ids { key, id } pairs to read\n   * @param opts you must specify group name and consumer name.\n   *              those must be created using the XGROUP command,\n   *              prior to invoking this command.  you may optionally\n   *              include a count of records to read, and the number\n   *              of milliseconds to block\n   */\n  xreadgroup(\n    key_xids: (XKeyIdGroup | XKeyIdGroupLike)[],\n    opts: XReadGroupOpts,\n  ): Promise<XReadReply>;\n\n  /**\n   * Trims the stream to the indicated number\n   * of elements.\n<pre>XTRIM mystream MAXLEN 1000</pre>\n   * @param key\n   * @param maxlen\n   */\n  xtrim(key: string, maxlen: XMaxlen): Promise<Integer>;\n\n  // SortedSet\n  bzpopmin(\n    timeout: number,\n    ...keys: string[]\n  ): Promise<[BulkString, BulkString, BulkString] | BulkNil>;\n  bzpopmax(\n    timeout: number,\n    ...keys: string[]\n  ): Promise<[BulkString, BulkString, BulkString] | BulkNil>;\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    score: number,\n    member: RedisValue,\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    score_members: [number, RedisValue][],\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    member_scores: Record<string | number, number>,\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zaddIncr(\n    key: string,\n    score: number,\n    member: RedisValue,\n    opts?: ZAddOpts,\n  ): Promise<Bulk>;\n  zcard(key: string): Promise<Integer>;\n  zcount(key: string, min: number, max: number): Promise<Integer>;\n  zincrby(\n    key: string,\n    increment: number,\n    member: RedisValue,\n  ): Promise<BulkString>;\n  zinter(keys: string[], opts?: ZInterOpts): Promise<Raw[]>;\n  zinter(key_weights: [string, number][], opts?: ZInterOpts): Promise<Raw[]>;\n  zinter(\n    key_weights: Record<string, number>,\n    opts?: ZInterOpts,\n  ): Promise<Raw[]>;\n  zinterstore(\n    destination: string,\n    keys: string[],\n    opts?: ZInterstoreOpts,\n  ): Promise<Integer>;\n  zinterstore(\n    destination: string,\n    key_weights: [string, number][],\n    opts?: ZInterstoreOpts,\n  ): Promise<Integer>;\n  zinterstore(\n    destination: string,\n    key_weights: Record<string, number>,\n    opts?: ZInterstoreOpts,\n  ): Promise<Integer>;\n  zlexcount(key: string, min: string, max: string): Promise<Integer>;\n  zpopmax(key: string, count?: number): Promise<BulkString[]>;\n  zpopmin(key: string, count?: number): Promise<BulkString[]>;\n  zrange(\n    key: string,\n    start: number,\n    stop: number,\n    opts?: ZRangeOpts,\n  ): Promise<BulkString[]>;\n  zrangebylex(\n    key: string,\n    min: string,\n    max: string,\n    opts?: ZRangeByLexOpts,\n  ): Promise<BulkString[]>;\n  zrangebyscore(\n    key: string,\n    min: number | string,\n    max: number | string,\n    opts?: ZRangeByScoreOpts,\n  ): Promise<BulkString[]>;\n  zrank(key: string, member: RedisValue): Promise<Integer | BulkNil>;\n  zrem(key: string, ...members: RedisValue[]): Promise<Integer>;\n  zremrangebylex(key: string, min: string, max: string): Promise<Integer>;\n  zremrangebyrank(key: string, start: number, stop: number): Promise<Integer>;\n  zremrangebyscore(\n    key: string,\n    min: number | string,\n    max: number | string,\n  ): Promise<Integer>;\n  zrevrange(\n    key: string,\n    start: number,\n    stop: number,\n    opts?: ZRangeOpts,\n  ): Promise<BulkString[]>;\n  zrevrangebylex(\n    key: string,\n    max: string,\n    min: string,\n    opts?: ZRangeByLexOpts,\n  ): Promise<BulkString[]>;\n  zrevrangebyscore(\n    key: string,\n    max: number | string,\n    min: number | string,\n    opts?: ZRangeByScoreOpts,\n  ): Promise<BulkString[]>;\n  zrevrank(key: string, member: RedisValue): Promise<Integer | BulkNil>;\n  zscan(\n    key: string,\n    cursor: number,\n    opts?: ZScanOpts,\n  ): Promise<[BulkString, BulkString[]]>;\n  zscore(key: string, member: RedisValue): Promise<Bulk>;\n  zunionstore(\n    destination: string,\n    keys: string[],\n    opts?: ZUnionstoreOpts,\n  ): Promise<Integer>;\n  zunionstore(\n    destination: string,\n    key_weights: [string, number][],\n    opts?: ZUnionstoreOpts,\n  ): Promise<Integer>;\n  zunionstore(\n    destination: string,\n    key_weights: Record<string, number>,\n    opts?: ZUnionstoreOpts,\n  ): Promise<Integer>;\n\n  // Client\n  /**\n   * This command controls the tracking of the keys in the next command executed by the connection.\n   * @see https://redis.io/commands/client-caching\n   */\n  clientCaching(mode: ClientCachingMode): Promise<SimpleString>;\n\n  /**\n   * Returns the name of the current connection which can be set by `clientSetName`.\n   * @see https://redis.io/commands/client-getname\n   */\n  clientGetName(): Promise<Bulk>;\n\n  /**\n   * Returns the client ID we are redirecting our tracking notifications to.\n   * @see https://redis.io/commands/client-getredir\n   */\n  clientGetRedir(): Promise<Integer>;\n\n  /**\n   * Returns the id of the current redis connection.\n   */\n  clientID(): Promise<Integer>;\n\n  /**\n   * Returns information and statistics about the current client connection in a mostly human readable format.\n   * @see https://redis.io/commands/client-info\n   */\n  clientInfo(): Promise<Bulk>;\n\n  /**\n   * Closes a given client connection.\n   * @see https://redis.io/commands/client-kill\n   */\n  clientKill(opts?: ClientKillOpts): Promise<Integer>;\n\n  /**\n   * Returns information and statistics about the client connections server in a mostly human readable format.\n   * @see https://redis.io/commands/client-list\n   */\n  clientList(opts?: ClientListOpts): Promise<Bulk>;\n\n  /**\n   * Suspend all the Redis clients for the specified amount of time (in milliseconds).\n   * @see https://redis.io/commands/client-pause\n   */\n  clientPause(timeout: number, mode?: ClientPauseMode): Promise<SimpleString>;\n\n  /**\n   * Sets a `connectionName` to the current connection.\n   * You can get the name of the current connection using `clientGetName()`.\n   * @see https://redis.io/commands/client-setname\n   */\n  clientSetName(connectionName: string): Promise<SimpleString>;\n\n  /**\n   * Enables the tracking feature for the Redis server that is used for server assisted client side caching.\n   * @see https://redis.io/commands/client-tracking\n   */\n  clientTracking(opts: ClientTrackingOpts): Promise<SimpleString>;\n\n  /**\n   * Returns information about the current client connection's use of the server assisted client side caching feature.\n   * @see https://redis.io/commands/client-trackinginfo\n   */\n  clientTrackingInfo(): Promise<ConditionalArray>;\n\n  /**\n   * This command can unblock, from a different connection, a client blocked in a blocking operation.\n   * @see https://redis.io/commands/client-unblock\n   */\n  clientUnblock(\n    id: number,\n    behaviour?: ClientUnblockingBehaviour,\n  ): Promise<Integer>;\n\n  /**\n   * Used to resume command processing for all clients that were paused by `clientPause`.\n   * @see https://redis.io/commands/client-unpause\n   */\n  clientUnpause(): Promise<SimpleString>;\n\n  // Cluster\n  /**\n   * @see https://redis.io/topics/cluster-spec\n   */\n  asking(): Promise<SimpleString>;\n  clusterAddSlots(...slots: number[]): Promise<SimpleString>;\n  clusterCountFailureReports(node_id: string): Promise<Integer>;\n  clusterCountKeysInSlot(slot: number): Promise<Integer>;\n  clusterDelSlots(...slots: number[]): Promise<SimpleString>;\n  clusterFailover(mode?: ClusterFailoverMode): Promise<SimpleString>;\n  clusterFlushSlots(): Promise<SimpleString>;\n  clusterForget(node_id: string): Promise<SimpleString>;\n  clusterGetKeysInSlot(slot: number, count: number): Promise<BulkString[]>;\n  clusterInfo(): Promise<BulkString>;\n  clusterKeySlot(key: string): Promise<Integer>;\n  clusterMeet(ip: string, port: number): Promise<SimpleString>;\n  clusterMyID(): Promise<BulkString>;\n  clusterNodes(): Promise<BulkString>;\n  clusterReplicas(node_id: string): Promise<BulkString[]>;\n  clusterReplicate(node_id: string): Promise<SimpleString>;\n  clusterReset(mode?: ClusterResetMode): Promise<SimpleString>;\n  clusterSaveConfig(): Promise<SimpleString>;\n  clusterSetSlot(\n    slot: number,\n    subcommand: ClusterSetSlotSubcommand,\n    node_id?: string,\n  ): Promise<SimpleString>;\n  clusterSlaves(node_id: string): Promise<BulkString[]>;\n  clusterSlots(): Promise<ConditionalArray>;\n  readonly(): Promise<SimpleString>;\n  readwrite(): Promise<SimpleString>;\n\n  // Server\n  aclCat(categoryname?: string): Promise<BulkString[]>;\n  aclDelUser(...usernames: string[]): Promise<Integer>;\n  aclGenPass(bits?: number): Promise<BulkString>;\n  aclGetUser(username: string): Promise<(BulkString | BulkString[])[]>;\n  aclHelp(): Promise<BulkString[]>;\n  aclList(): Promise<BulkString[]>;\n  aclLoad(): Promise<SimpleString>;\n  aclLog(count: number): Promise<BulkString[]>;\n  aclLog(mode: ACLLogMode): Promise<SimpleString>;\n  aclSave(): Promise<SimpleString>;\n  aclSetUser(username: string, ...rules: string[]): Promise<SimpleString>;\n  aclUsers(): Promise<BulkString[]>;\n  aclWhoami(): Promise<BulkString>;\n  bgrewriteaof(): Promise<SimpleString>;\n  bgsave(): Promise<SimpleString>;\n  command(): Promise<\n    [BulkString, Integer, BulkString[], Integer, Integer, Integer][]\n  >;\n  commandCount(): Promise<Integer>;\n  commandGetKeys(): Promise<BulkString[]>;\n  commandInfo(\n    ...command_names: string[]\n  ): Promise<\n    ([BulkString, Integer, BulkString[], Integer, Integer, Integer] | BulkNil)[]\n  >;\n  configGet(parameter: string): Promise<BulkString[]>;\n  configResetStat(): Promise<SimpleString>;\n  configRewrite(): Promise<SimpleString>;\n  configSet(parameter: string, value: string): Promise<SimpleString>;\n  dbsize(): Promise<Integer>;\n  debugObject(key: string): Promise<SimpleString>;\n  debugSegfault(): Promise<SimpleString>;\n  flushall(async?: boolean): Promise<SimpleString>;\n  flushdb(async?: boolean): Promise<SimpleString>;\n  info(section?: string): Promise<BulkString>;\n  lastsave(): Promise<Integer>;\n  memoryDoctor(): Promise<BulkString>;\n  memoryHelp(): Promise<BulkString[]>;\n  memoryMallocStats(): Promise<BulkString>;\n  memoryPurge(): Promise<SimpleString>;\n  memoryStats(): Promise<ConditionalArray>;\n  memoryUsage(key: string, opts?: MemoryUsageOpts): Promise<Integer>;\n  moduleList(): Promise<BulkString[]>;\n  moduleLoad(path: string, ...args: string[]): Promise<SimpleString>;\n  moduleUnload(name: string): Promise<SimpleString>;\n  monitor(): void;\n  replicaof(host: string, port: number): Promise<SimpleString>;\n  replicaofNoOne(): Promise<SimpleString>;\n  role(): Promise<RoleReply>;\n  save(): Promise<SimpleString>;\n  shutdown(mode?: ShutdownMode): Promise<SimpleString>;\n  slaveof(host: string, port: number): Promise<SimpleString>;\n  slaveofNoOne(): Promise<SimpleString>;\n  slowlog(subcommand: string, ...args: string[]): Promise<ConditionalArray>;\n  swapdb(index1: number, index2: number): Promise<SimpleString>;\n  sync(): void;\n  time(): Promise<[BulkString, BulkString]>;\n\n  // Scripting\n  eval(script: string, keys: string[], args: RedisValue[]): Promise<Raw>;\n  evalsha(sha1: string, keys: string[], args: RedisValue[]): Promise<Raw>;\n  scriptDebug(mode: ScriptDebugMode): Promise<SimpleString>;\n  scriptExists(...sha1s: string[]): Promise<Integer[]>;\n  scriptFlush(): Promise<SimpleString>;\n  scriptKill(): Promise<SimpleString>;\n  scriptLoad(script: string): Promise<SimpleString>;\n\n  // Transactions\n  discard(): Promise<SimpleString>;\n  exec(): Promise<ConditionalArray>;\n  multi(): Promise<SimpleString>;\n  unwatch(): Promise<SimpleString>;\n  watch(...keys: string[]): Promise<SimpleString>;\n\n  // Latency\n  latencyDoctor(): Promise<BulkString>;\n\n  // Pipeline\n  tx(): RedisPipeline;\n  pipeline(): RedisPipeline;\n}\n"
  },
  {
    "path": "connection.ts",
    "content": "import type { Backoff } from \"./backoff.ts\";\nimport type { ConnectionEventMap } from \"./events.ts\";\nimport type { ErrorReplyError } from \"./errors.ts\";\nimport type { TypedEventTarget } from \"./internal/typed_event_target.ts\";\nimport type {\n  kUnstableCreateProtocol,\n  kUnstablePipeline,\n  kUnstableProtover,\n  kUnstableReadReply,\n  kUnstableStartReadLoop,\n  kUnstableWriteCommand,\n} from \"./internal/symbols.ts\";\nimport type { Command, Protocol } from \"./protocol/shared/protocol.ts\";\nimport type {\n  Protover,\n  RedisReply,\n  RedisValue,\n} from \"./protocol/shared/types.ts\";\n\nexport interface SendCommandOptions {\n  /**\n   * When this option is set, simple or bulk string replies are returned as `Uint8Array` type.\n   *\n   * @default false\n   */\n  returnUint8Arrays?: boolean;\n}\n\nexport interface Connection extends TypedEventTarget<ConnectionEventMap> {\n  /** @deprecated */\n  name: string | null;\n  isClosed: boolean;\n  isConnected: boolean;\n  close(): void;\n  [Symbol.dispose](): void;\n  connect(): Promise<void>;\n  reconnect(): Promise<void>;\n  sendCommand(\n    command: string,\n    args?: Array<RedisValue>,\n    options?: SendCommandOptions,\n  ): Promise<RedisReply>;\n  /**\n   * @private\n   */\n  [kUnstableReadReply](returnsUint8Arrays?: boolean): Promise<RedisReply>;\n  /**\n   * @private\n   */\n  [kUnstableWriteCommand](command: Command): Promise<void>;\n  /**\n   * @private\n   */\n  [kUnstablePipeline](\n    commands: Array<Command>,\n  ): Promise<Array<RedisReply | ErrorReplyError>>;\n  /**\n   * @private\n   */\n  [kUnstableStartReadLoop](\n    binaryMode?: boolean,\n  ): AsyncIterableIterator<RedisReply>;\n}\n\nexport interface RedisConnectionOptions {\n  tls?: boolean;\n  /**\n   * A list of root certificates, implies {@linkcode RedisConnectionOptions.tls}\n   */\n  caCerts?: string[];\n  db?: number;\n  password?: string;\n  username?: string;\n  name?: string;\n  /**\n   * @default 10\n   */\n  maxRetryCount?: number;\n  backoff?: Backoff;\n  /**\n   * When this option is set, a `PING` command is sent every specified number of seconds.\n   */\n  healthCheckInterval?: number;\n\n  /**\n   * An {@linkcode AbortSignal} which is returned by this function is used to abort an ongoing connection process. With this option, you can implement connection timeout, etc.\n   *\n   * Works only in Deno v2.3 or later.\n   */\n  signal?: () => AbortSignal;\n\n  /**\n   * If `true`, disables Nagle's algorithm.\n   *\n   * @default false\n   */\n  noDelay?: boolean;\n\n  /**\n   * @private\n   */\n  [kUnstableCreateProtocol]?: (conn: Deno.Conn) => Protocol;\n\n  /**\n   * @private\n   */\n  [kUnstableProtover]?: Protover;\n}\n"
  },
  {
    "path": "default_client.ts",
    "content": "import type { Client } from \"./client.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisSubscription,\n  SubscribeCommand,\n} from \"./subscription.ts\";\nimport type { Connection, SendCommandOptions } from \"./connection.ts\";\nimport { DefaultRedisSubscription } from \"./default_subscription.ts\";\nimport type { RedisReply, RedisValue } from \"./protocol/shared/types.ts\";\n\nexport function createDefaultClient(connection: Connection): Client {\n  return new DefaultClient(connection);\n}\n\nclass DefaultClient implements Client {\n  constructor(readonly connection: Connection) {}\n\n  exec(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<RedisReply> {\n    return this.connection.sendCommand(command, args);\n  }\n\n  sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ) {\n    return this.connection.sendCommand(command, args, options);\n  }\n\n  async subscribe<\n    TMessage extends PubSubMessageType = DefaultPubSubMessageType,\n  >(\n    command: SubscribeCommand,\n    ...channelsOrPatterns: Array<string>\n  ): Promise<RedisSubscription<TMessage>> {\n    const subscription = new DefaultRedisSubscription<TMessage>(\n      this.connection,\n    );\n    switch (command) {\n      case \"SUBSCRIBE\":\n        await subscription.subscribe(...channelsOrPatterns);\n        break;\n      case \"PSUBSCRIBE\":\n        await subscription.psubscribe(...channelsOrPatterns);\n        break;\n    }\n    return subscription;\n  }\n\n  close(): void {\n    this.connection.close();\n  }\n}\n"
  },
  {
    "path": "default_connection.ts",
    "content": "import type { Backoff } from \"./backoff.ts\";\nimport { exponentialBackoff } from \"./backoff.ts\";\nimport type {\n  Connection,\n  RedisConnectionOptions,\n  SendCommandOptions,\n} from \"./connection.ts\";\nimport {\n  ErrorReplyError,\n  InvalidStateError,\n  isRetriableError,\n} from \"./errors.ts\";\nimport type { ConnectionEventMap } from \"./events.ts\";\nimport {\n  kUnstableCreateProtocol,\n  kUnstablePipeline,\n  kUnstableProtover,\n  kUnstableReadReply,\n  kUnstableStartReadLoop,\n  kUnstableWriteCommand,\n} from \"./internal/symbols.ts\";\nimport type { TypedEventTarget } from \"./internal/typed_event_target.ts\";\nimport {\n  createTypedEventTarget,\n  dispatchEvent,\n} from \"./internal/typed_event_target.ts\";\nimport { kEmptyRedisArgs } from \"./protocol/shared/command.ts\";\nimport type { Command, Protocol } from \"./protocol/shared/protocol.ts\";\nimport { Protocol as DenoStreamsProtocol } from \"./protocol/deno_streams/mod.ts\";\nimport type { RedisReply, RedisValue } from \"./protocol/shared/types.ts\";\nimport { delay } from \"./deps/std/async.ts\";\n\nexport function createRedisConnection(\n  hostname: string,\n  port: number | string | undefined,\n  options: RedisConnectionOptions,\n): Connection {\n  return new RedisConnection(hostname, port ?? 6379, options);\n}\n\ninterface PendingCommand {\n  execute: () => Promise<RedisReply>;\n  resolve: (reply: RedisReply) => void;\n  reject: (error: unknown) => void;\n}\n\nclass RedisConnection\n  implements Connection, TypedEventTarget<ConnectionEventMap> {\n  name: string | null = null;\n  private maxRetryCount = 10;\n\n  private readonly hostname: string;\n  private readonly port: number | string;\n  private _isClosed = false;\n  private _isConnected = false;\n  private backoff: Backoff;\n\n  private commandQueue: PendingCommand[] = [];\n  #conn!: Deno.Conn;\n  #protocol!: Protocol;\n  #eventTarget = createTypedEventTarget<ConnectionEventMap>();\n  #connectingPromise?: PromiseWithResolvers<void>;\n\n  get isClosed(): boolean {\n    return this._isClosed;\n  }\n\n  get isConnected(): boolean {\n    return this._isConnected;\n  }\n\n  get isRetriable(): boolean {\n    return this.maxRetryCount > 0;\n  }\n\n  constructor(\n    hostname: string,\n    port: number | string,\n    private options: RedisConnectionOptions,\n  ) {\n    this.hostname = hostname;\n    this.port = port;\n    if (options.name) {\n      this.name = options.name;\n    }\n    if (options.maxRetryCount != null) {\n      this.maxRetryCount = options.maxRetryCount;\n    }\n    this.backoff = options.backoff ?? exponentialBackoff();\n  }\n\n  private async authenticate(\n    username: string | undefined,\n    password: string,\n  ): Promise<void> {\n    try {\n      // TODO: Use `HELLO` instead of `AUTH`\n      password && username\n        ? await this.#sendCommandImmediately(\"AUTH\", [username, password])\n        : await this.#sendCommandImmediately(\"AUTH\", [password]);\n    } catch (error) {\n      if (error instanceof ErrorReplyError) {\n        const authError = new AuthenticationError(\"Authentication failed\", {\n          cause: error,\n        });\n        dispatchEvent(this.#eventTarget, \"error\", { error: authError });\n        throw authError;\n      } else {\n        dispatchEvent(this.#eventTarget, \"error\", { error });\n        throw error;\n      }\n    }\n  }\n\n  private async selectDb(\n    db: number | undefined = this.options.db,\n  ): Promise<void> {\n    if (!db) throw new Error(\"The database index is undefined.\");\n    await this.#sendCommandImmediately(\"SELECT\", [db]);\n  }\n\n  private enqueueCommand(\n    command: PendingCommand,\n  ) {\n    this.commandQueue.push(command);\n    if (!this.#isProcessingQueuedCommands) {\n      this.#isProcessingQueuedCommands = true;\n      this.processCommandQueue();\n    }\n  }\n\n  sendCommand(\n    command: string,\n    args?: Array<RedisValue>,\n    options?: SendCommandOptions,\n  ): Promise<RedisReply> {\n    const execute = () =>\n      this.#protocol.sendCommand(\n        command,\n        args ?? kEmptyRedisArgs,\n        options?.returnUint8Arrays,\n      );\n    const { promise, resolve, reject } = Promise.withResolvers<RedisReply>();\n    this.enqueueCommand({ execute, resolve, reject });\n\n    return promise;\n  }\n\n  /**\n   * Executes a command immediately, bypassing the queue.\n   */\n  #sendCommandImmediately(\n    command: string,\n    args?: Array<RedisValue>,\n  ): Promise<RedisReply> {\n    const isConnecting = this.#connectingPromise != null;\n    if (!isConnecting) {\n      return Promise.reject(\n        new InvalidStateError(\n          `Unexpected inline command execution detected (command: ${command})`,\n        ),\n      );\n    }\n    return this.#protocol.sendCommand(\n      command,\n      args ?? kEmptyRedisArgs,\n    );\n  }\n\n  addEventListener<K extends keyof ConnectionEventMap>(\n    type: K,\n    callback: (event: CustomEvent<ConnectionEventMap[K]>) => void,\n    options?: AddEventListenerOptions | boolean,\n  ): void {\n    return this.#eventTarget.addEventListener(\n      type,\n      callback as (event: Event) => void,\n      options,\n    );\n  }\n\n  removeEventListener<K extends keyof ConnectionEventMap>(\n    type: K,\n    callback: (event: CustomEvent<ConnectionEventMap[K]>) => void,\n    options?: EventListenerOptions | boolean,\n  ): void {\n    return this.#eventTarget.removeEventListener(\n      type,\n      callback as (event: Event) => void,\n      options,\n    );\n  }\n\n  [kUnstableReadReply](returnsUint8Arrays?: boolean): Promise<RedisReply> {\n    return this.#protocol.readReply(returnsUint8Arrays);\n  }\n\n  [kUnstablePipeline](commands: Array<Command>): Promise<RedisReply[]> {\n    const { promise, resolve, reject } = Promise.withResolvers<\n      RedisReply[]\n    >();\n    const execute = () => this.#protocol.pipeline(commands);\n    this.enqueueCommand({ execute, resolve, reject } as PendingCommand);\n    return promise;\n  }\n\n  [kUnstableWriteCommand](command: Command): Promise<void> {\n    return this.#protocol.writeCommand(command);\n  }\n\n  async *[kUnstableStartReadLoop](\n    binaryMode?: boolean,\n  ): AsyncIterableIterator<RedisReply> {\n    let forceReconnect = false;\n    while (this.isConnected) {\n      try {\n        let rep: RedisReply;\n        try {\n          rep = await this[kUnstableReadReply](binaryMode);\n        } catch (err) {\n          if (this.isClosed) {\n            // Connection already closed by the user.\n            break;\n          }\n          throw err; // Connection may have been unintentionally closed.\n        }\n        yield rep;\n      } catch (error) {\n        if (isRetriableError(error)) {\n          forceReconnect = true;\n        } else throw error;\n      } finally {\n        if ((!this.isClosed && !this.isConnected) || forceReconnect) {\n          forceReconnect = false;\n          await this.reconnect();\n        }\n      }\n    }\n  }\n\n  /**\n   * Connect to Redis server\n   */\n  connect(): Promise<void> {\n    if (this.#connectingPromise) {\n      return this.#connectingPromise.promise;\n    }\n    const promiseWithResolvers = Promise.withResolvers<void>();\n    this.#connectingPromise = promiseWithResolvers;\n    (async () => {\n      try {\n        await this.#connect(0);\n        promiseWithResolvers.resolve();\n        this.#connectingPromise = undefined;\n      } catch (error) {\n        promiseWithResolvers.reject(error);\n        this.#connectingPromise = undefined;\n      }\n    })();\n    return promiseWithResolvers.promise;\n  }\n\n  async #connect(retryCount: number) {\n    try {\n      const signal: AbortSignal | undefined = this.options?.signal?.() ??\n        undefined;\n      const dialOpts: Deno.ConnectOptions = {\n        hostname: this.hostname,\n        port: parsePortLike(this.port),\n        signal,\n      };\n      const conn = this.options?.tls || this.options?.caCerts != null\n        ? await Deno.connectTls({\n          ...dialOpts,\n          caCerts: this.options?.caCerts,\n        })\n        : await Deno.connect(dialOpts);\n      if (this.options?.noDelay && \"setNoDelay\" in conn) {\n        conn.setNoDelay();\n      }\n\n      this.#conn = conn;\n      this.#protocol = this.options?.[kUnstableCreateProtocol]?.(conn) ??\n        new DenoStreamsProtocol(conn);\n\n      this._isClosed = false;\n      this._isConnected = true;\n      dispatchEvent(this.#eventTarget, \"connect\", undefined);\n\n      try {\n        if (this.options.password != null) {\n          await this.authenticate(this.options.username, this.options.password);\n        }\n        if (this.options[kUnstableProtover] != null) {\n          await this.#sendCommandImmediately(\"HELLO\", [\n            this.options[kUnstableProtover],\n          ]);\n        }\n        if (this.options.db) {\n          await this.selectDb(this.options.db);\n        }\n      } catch (error) {\n        this.#close();\n        throw error;\n      }\n\n      dispatchEvent(this.#eventTarget, \"ready\", undefined);\n\n      this.#enableHealthCheckIfNeeded();\n    } catch (error) {\n      if (error instanceof AuthenticationError) {\n        dispatchEvent(this.#eventTarget, \"error\", { error });\n        dispatchEvent(this.#eventTarget, \"end\", undefined);\n        throw (error.cause ?? error);\n      }\n\n      const backoff = this.backoff(retryCount);\n      retryCount++;\n      if (retryCount >= this.maxRetryCount) {\n        dispatchEvent(this.#eventTarget, \"error\", { error: error as Error });\n        dispatchEvent(this.#eventTarget, \"end\", undefined);\n        throw error;\n      }\n      dispatchEvent(this.#eventTarget, \"reconnecting\", { delay: backoff });\n      await delay(backoff);\n      await this.#connect(retryCount);\n    }\n  }\n\n  close() {\n    return this[Symbol.dispose]();\n  }\n\n  [Symbol.dispose](): void {\n    return this.#close(false);\n  }\n\n  #close(canReconnect = false) {\n    const isClosedAlready = this._isClosed;\n\n    this._isClosed = true;\n    this._isConnected = false;\n    try {\n      this.#conn!.close();\n    } catch (error) {\n      if (!(error instanceof Deno.errors.BadResource)) {\n        dispatchEvent(this.#eventTarget, \"error\", { error: error as Error });\n        throw error;\n      }\n    } finally {\n      if (!isClosedAlready) {\n        dispatchEvent(this.#eventTarget, \"close\", undefined);\n\n        if (!canReconnect) {\n          dispatchEvent(this.#eventTarget, \"end\", undefined);\n        }\n      }\n    }\n  }\n\n  async reconnect(): Promise<void> {\n    try {\n      await this.sendCommand(\"PING\");\n      this._isConnected = true;\n    } catch (error) {\n      dispatchEvent(this.#eventTarget, \"error\", { error });\n      this.#close(true);\n      await this.connect();\n      await this.sendCommand(\"PING\");\n    }\n  }\n\n  #isProcessingQueuedCommands = false;\n  private async processCommandQueue() {\n    const [command] = this.commandQueue;\n    if (!command) {\n      this.#isProcessingQueuedCommands = false;\n      return;\n    }\n\n    try {\n      const reply = await command.execute();\n      command.resolve(reply);\n    } catch (error) {\n      if (\n        !isRetriableError(error) ||\n        this.isManuallyClosedByUser()\n      ) {\n        dispatchEvent(this.#eventTarget, \"error\", { error });\n        return command.reject(error);\n      }\n\n      let backoff = 0;\n      for (let i = 0; i < this.maxRetryCount; i++) {\n        // Try to reconnect to the server and retry the command\n        this.#close(true);\n        try {\n          dispatchEvent(this.#eventTarget, \"reconnecting\", { delay: backoff });\n          await this.connect();\n          const reply = await command.execute();\n          return command.resolve(reply);\n        } catch (error) {\n          dispatchEvent(this.#eventTarget, \"error\", { error }); // TODO: use `AggregateError`?\n          backoff = this.backoff(i);\n          await delay(backoff);\n        }\n      }\n\n      dispatchEvent(this.#eventTarget, \"error\", { error });\n      command.reject(error);\n    } finally {\n      this.commandQueue.shift();\n      this.processCommandQueue();\n    }\n  }\n\n  private isManuallyClosedByUser(): boolean {\n    return this._isClosed && !this._isConnected;\n  }\n\n  #enableHealthCheckIfNeeded() {\n    const { healthCheckInterval } = this.options;\n    if (healthCheckInterval == null) {\n      return;\n    }\n\n    const ping = async () => {\n      if (this.isManuallyClosedByUser()) {\n        return;\n      }\n\n      try {\n        await this.sendCommand(\"PING\");\n        this._isConnected = true;\n      } catch (_error) {\n        // TODO: notify the user of an error\n        this._isConnected = false;\n      } finally {\n        setTimeout(ping, healthCheckInterval);\n      }\n    };\n\n    setTimeout(ping, healthCheckInterval);\n  }\n}\n\nclass AuthenticationError extends Error {}\n\nfunction parsePortLike(port: string | number | undefined): number {\n  let parsedPort: number;\n  if (typeof port === \"string\") {\n    parsedPort = parseInt(port);\n  } else if (typeof port === \"number\") {\n    parsedPort = port;\n  } else {\n    parsedPort = 6379;\n  }\n  if (!Number.isSafeInteger(parsedPort)) {\n    throw new Error(\"Port is invalid\");\n  }\n  return parsedPort;\n}\n"
  },
  {
    "path": "default_subscription.ts",
    "content": "import { decoder } from \"./internal/encoding.ts\";\nimport {\n  kUnstableStartReadLoop,\n  kUnstableWriteCommand,\n} from \"./internal/symbols.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisPubSubMessage,\n  RedisSubscription,\n} from \"./subscription.ts\";\nimport type { Connection } from \"./connection.ts\";\nimport type { Binary } from \"./protocol/shared/types.ts\";\n\nexport class DefaultRedisSubscription<\n  TMessage extends PubSubMessageType = DefaultPubSubMessageType,\n> implements RedisSubscription<TMessage> {\n  get isConnected(): boolean {\n    return this.connection.isConnected;\n  }\n\n  get isClosed(): boolean {\n    return this.connection.isClosed;\n  }\n\n  private channels = Object.create(null);\n  private patterns = Object.create(null);\n\n  constructor(private readonly connection: Connection) {}\n\n  async psubscribe(...patterns: string[]) {\n    await this.#writeCommand(\"PSUBSCRIBE\", patterns);\n    for (const pat of patterns) {\n      this.patterns[pat] = true;\n    }\n  }\n\n  async punsubscribe(...patterns: string[]) {\n    await this.#writeCommand(\"PUNSUBSCRIBE\", patterns);\n    for (const pat of patterns) {\n      delete this.patterns[pat];\n    }\n  }\n\n  async subscribe(...channels: string[]) {\n    await this.#writeCommand(\"SUBSCRIBE\", channels);\n    for (const chan of channels) {\n      this.channels[chan] = true;\n    }\n  }\n\n  async unsubscribe(...channels: string[]) {\n    await this.#writeCommand(\"UNSUBSCRIBE\", channels);\n    for (const chan of channels) {\n      delete this.channels[chan];\n    }\n  }\n\n  receive(): AsyncIterableIterator<RedisPubSubMessage<TMessage>> {\n    return this.#receive(false);\n  }\n\n  receiveBuffers(): AsyncIterableIterator<RedisPubSubMessage<Binary>> {\n    return this.#receive(true);\n  }\n\n  async *#receive<\n    T = TMessage,\n  >(\n    binaryMode: boolean,\n  ): AsyncIterableIterator<\n    RedisPubSubMessage<T>\n  > {\n    const onConnectionRecovered = async () => {\n      if (Object.keys(this.channels).length > 0) {\n        await this.subscribe(...Object.keys(this.channels));\n      }\n      if (Object.keys(this.patterns).length > 0) {\n        await this.psubscribe(...Object.keys(this.patterns));\n      }\n    };\n    this.connection.addEventListener(\"connect\", onConnectionRecovered);\n    const iter = this.connection[kUnstableStartReadLoop](binaryMode);\n    try {\n      for await (const _rep of iter) {\n        const rep = _rep as ([string | Binary, string | Binary, T] | [\n          string | Binary,\n          string | Binary,\n          string | Binary,\n          T,\n        ]);\n        const event = rep[0] instanceof Uint8Array\n          ? decoder.decode(rep[0])\n          : rep[0];\n        if (event === \"message\" && rep.length === 3) {\n          const channel = rep[1] instanceof Uint8Array\n            ? decoder.decode(rep[1])\n            : rep[1];\n          const message = rep[2];\n          yield {\n            channel,\n            message,\n          };\n        } else if (event === \"pmessage\" && rep.length === 4) {\n          const pattern = rep[1] instanceof Uint8Array\n            ? decoder.decode(rep[1])\n            : rep[1];\n          const channel = rep[2] instanceof Uint8Array\n            ? decoder.decode(rep[2])\n            : rep[2];\n          const message = rep[3];\n          yield {\n            pattern,\n            channel,\n            message,\n          };\n        }\n      }\n    } finally {\n      this.connection.removeEventListener(\n        \"connect\",\n        onConnectionRecovered,\n      );\n    }\n  }\n\n  close() {\n    this.connection.close();\n  }\n\n  async #writeCommand(command: string, args: Array<string>): Promise<void> {\n    await this.connection[kUnstableWriteCommand]({ command, args });\n  }\n}\n"
  },
  {
    "path": "deno.json",
    "content": "{\n  \"name\": \"@db/redis\",\n  \"version\": \"0.41.0\",\n  \"exports\": {\n    \".\": \"./mod.ts\",\n    \"./experimental/pool\": \"./experimental/pool/mod.ts\",\n    \"./experimental/cluster\": \"./experimental/cluster/mod.ts\",\n    \"./experimental/web-streams-connection\": \"./experimental/web_streams_connection/mod.ts\"\n  },\n  \"exclude\": [\n    \"benchmark/node_modules\",\n    \"tmp\"\n  ],\n  \"lint\": {\n    \"exclude\": [\n      \"benchmark/benchmark.js\",\n      \"benchmark/ioredis.js\"\n    ],\n    \"plugins\": [\"jsr:@uki00a/deno-lint-plugin-extra-rules@0.9.0\"],\n    \"rules\": { \"include\": [\"no-console\"] }\n  },\n  \"test\": {\n    \"exclude\": [\"benchmark/\", \"vendor/\"],\n    \"permissions\": {\n      \"net\": [\"127.0.0.1\"],\n      \"read\": [\"tests\"],\n      \"write\": [\"tests/tmp\"],\n      \"run\": [\"redis-server\", \"redis-cli\"],\n      \"env\": [\"REDIS_VERSION\"]\n    }\n  },\n  \"permissions\": {\n    \"bench:deno-redis\": {\n      \"net\": [\"127.0.0.1:6379\"],\n      \"env\": [\n        \"NODE_DISABLE_COLORS\",\n        \"TERM\",\n        \"TERM_PROGRAM\",\n        \"GRACEFUL_FS_PLATFORM\",\n        \"TEST_GRACEFUL_FS_GLOBAL_PATCH\"\n      ],\n      \"write\": [\"tmp\"]\n    }\n  },\n  \"publish\": {\n    \"exclude\": [\n      \".denov\",\n      \".editorconfig\",\n      \".octocov.yml\",\n      \".github\",\n      \"import_map.dev.json\",\n      \"import_map.test.json\",\n      \"benchmark/\",\n      \"tests/\",\n      \"**/*_test.ts\",\n      \"tools/\"\n    ]\n  },\n  \"minimumDependencyAge\": 1440,\n  \"tasks\": {\n    \"lock\": \"deno cache --reload --import-map=import_map.dev.json mod.ts experimental/**/mod.ts tests/**/*.ts benchmark/*.ts\",\n    \"check\": {\n      \"dependencies\": [\"check:lint\", \"check:deno.json\", \"typecheck\"]\n    },\n    \"check:lint\": \"deno lint && deno fmt --check\",\n    \"check:deno.json\": \"deno run -R=deno.json --import-map=import_map.dev.json @uki00a/deno-json-lint\",\n    \"typecheck\": \"cat deno.json | jq '.exports.[]' | xargs deno check\",\n    \"test\": \"DENO_FUTURE=1 deno test --permission-set --coverage=coverage --trace-leaks --frozen-lockfile\",\n    \"test:doc\": \"deno check --doc-only --import-map=import_map.test.json README.md experimental/cluster/README.md\",\n    \"bench:deno-redis\": \"DENO_NO_PACKAGE_JSON=1 deno run --unstable --permission-set=bench:deno-redis --import-map=import_map.dev.json benchmark/deno-redis.ts\",\n    \"bench:ioredis\": \"node benchmark/ioredis.js\"\n  },\n  \"deno-json-lint\": {\n    \"rules\": {\n      \"no-restricted-fields\": [\"error\", {\n        \"fields\": {\n          \"imports\": \"Use `import_map.dev.json` instead.\"\n        }\n      }]\n    }\n  }\n}\n"
  },
  {
    "path": "deps/cluster-key-slot.js",
    "content": "export { default as calculateSlot } from \"npm:cluster-key-slot@1.1.0\";\n"
  },
  {
    "path": "deps/std/assert.ts",
    "content": "export * from \"jsr:@std/assert@^1\";\n"
  },
  {
    "path": "deps/std/async.ts",
    "content": "export * from \"jsr:@std/async@^1/delay\";\n"
  },
  {
    "path": "deps/std/bytes.ts",
    "content": "export * from \"jsr:@std/bytes@^1/concat\";\n"
  },
  {
    "path": "deps/std/collections.ts",
    "content": "export { distinctBy } from \"jsr:@std/collections@^1/distinct-by\";\n"
  },
  {
    "path": "deps/std/io.ts",
    "content": "export { BufReader } from \"jsr:@std/io@0.224.5/buf-reader\";\nexport { BufWriter } from \"jsr:@std/io@0.224.5/buf-writer\";\nexport { readerFromStreamReader } from \"jsr:@std/io@0.224.5/reader-from-stream-reader\";\nexport { readAll } from \"jsr:@std/io@0.224.5/read-all\";\n"
  },
  {
    "path": "deps/std/random.ts",
    "content": "export { sample } from \"jsr:@std/random@0.1.0/sample\";\nexport { shuffle } from \"jsr:@std/random@0.1.0/shuffle\";\n"
  },
  {
    "path": "deps/std/testing.ts",
    "content": "export * from \"jsr:@std/testing@^1/bdd\";\nexport type * from \"jsr:@std/testing@^1/types\";\nexport { assertType } from \"jsr:@std/testing@^1/types\";\n"
  },
  {
    "path": "errors.ts",
    "content": "export class EOFError extends Error {}\n\nexport class ConnectionClosedError extends Error {}\n\nexport class SubscriptionClosedError extends Error {}\n\nexport class ErrorReplyError extends Error {}\nexport class NotImplementedError extends Error {\n  constructor(message?: string) {\n    super(message ? `Not implemented: ${message}` : \"Not implemented\");\n  }\n}\n\nexport class InvalidStateError extends Error {\n  constructor(message?: string) {\n    const base = \"Invalid state\";\n    super(message ? `${base}: ${message}` : base);\n  }\n}\n\nexport function isRetriableError(error: unknown): boolean {\n  return (error instanceof Deno.errors.BadResource ||\n    error instanceof Deno.errors.BrokenPipe ||\n    error instanceof Deno.errors.ConnectionAborted ||\n    error instanceof Deno.errors.ConnectionRefused ||\n    error instanceof Deno.errors.ConnectionReset ||\n    error instanceof Deno.errors.UnexpectedEof ||\n    error instanceof EOFError);\n}\n"
  },
  {
    "path": "events.ts",
    "content": "export type ConnectionEvent = Record<string, unknown>;\n\nexport type ConnectionErrorEventDetails = {\n  error: unknown;\n};\n\nexport type ConnectionReconnectingEventDetails = {\n  delay: number;\n};\n\nexport type ConnectionEventMap = {\n  error: ConnectionErrorEventDetails;\n  connect: unknown;\n  reconnecting: ConnectionReconnectingEventDetails;\n  ready: unknown;\n  close: unknown;\n  end: unknown;\n};\n\nexport type ConnectionEventType =\n  | \"error\"\n  | \"connect\"\n  | \"reconnecting\"\n  | \"ready\"\n  | \"close\"\n  | \"end\";\n"
  },
  {
    "path": "executor.ts",
    "content": "import type { Client } from \"./client.ts\";\n\n/** @deprecated Use {@linkcode Client} instead. */\nexport type CommandExecutor = Client;\n"
  },
  {
    "path": "experimental/README.md",
    "content": "# Experimental features\n\ndeno-redis has some experimental features:\n\n- [Redis Cluster client](cluster/README.md)\n\n**These experimental features may be subject to breaking changes even after a\nmajor release.**\n"
  },
  {
    "path": "experimental/cluster/README.md",
    "content": "# experimental/cluster\n\n[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/redis/experimental/cluster/mod.ts)\n\nThis module provides a client impelementation for\n[the Redis Cluster](https://redis.io/topics/cluster-tutorial).\n\nThe implementation is based on the\n[antirez/redis-rb-cluster](https://github.com/antirez/redis-rb-cluster).\n\n## Usage\n\n```typescript\nimport { connect } from \"@db/redis/experimental/cluster\";\n\nconst cluster = await connect({\n  nodes: [\n    {\n      hostname: \"127.0.0.1\",\n      port: 7000,\n    },\n    {\n      hostname: \"127.0.0.1\",\n      port: 7001,\n    },\n  ],\n});\n\nawait cluster.get(\"{foo}bar\");\n```\n"
  },
  {
    "path": "experimental/cluster/mod.ts",
    "content": "/**\n * @module\n * @experimental **NOTE**: This is an unstable module.\n *\n * Based on https://github.com/antirez/redis-rb-cluster which is licensed as follows:\n *\n * Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>\n *\n * Permission is hereby granted, free of charge, to any person obtaining\n * a copy of this software and associated documentation files (the\n * \"Software\"), to deal in the Software without restriction, including\n * without limitation the rights to use, copy, modify, merge, publish,\n * distribute, sublicense, and/or sell copies of the Software, and to\n * permit persons to whom the Software is furnished to do so, subject to\n * the following conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport { connect, create } from \"../../redis.ts\";\nimport type { RedisConnectOptions } from \"../../redis.ts\";\nimport type { Client } from \"../../client.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisSubscription,\n  SubscribeCommand,\n} from \"../../subscription.ts\";\nimport type { Connection, SendCommandOptions } from \"../../connection.ts\";\nimport type { Redis } from \"../../redis.ts\";\nimport type { RedisReply, RedisValue } from \"../../protocol/shared/types.ts\";\nimport { ErrorReplyError, NotImplementedError } from \"../../errors.ts\";\nimport { delay } from \"../../deps/std/async.ts\";\nimport { distinctBy } from \"../../deps/std/collections.ts\";\nimport { sample, shuffle } from \"../../deps/std/random.ts\";\nimport { calculateSlot } from \"../../deps/cluster-key-slot.js\";\n\nexport interface ClusterConnectOptions {\n  nodes: Array<NodeOptions>;\n  maxConnections?: number;\n  newRedis?: (opts: RedisConnectOptions) => Promise<Redis>;\n}\n\nexport interface NodeOptions {\n  hostname: string;\n  port?: number;\n}\n\ninterface SlotMap {\n  [slot: number]: ClusterNode;\n}\n\nclass ClusterNode {\n  readonly name: string;\n\n  constructor(readonly hostname: string, readonly port: number) {\n    this.name = `${hostname}:${port}`;\n  }\n\n  static parseIPAndPort(ipAndPort: string): ClusterNode {\n    const [ip, port] = ipAndPort.split(\":\");\n    const node = new ClusterNode(\n      ip,\n      parseInt(port),\n    );\n    return node;\n  }\n}\n\nconst kRedisClusterRequestTTL = 16;\n\nclass ClusterError extends Error {}\n\nclass ClusterClient implements Client {\n  #nodeBySlot!: SlotMap;\n  #startupNodes: ClusterNode[];\n  #refreshTableASAP?: boolean;\n  #maxConnections: number;\n  #connectionByNodeName: { [name: string]: Redis } = {};\n  #newRedis: (opts: RedisConnectOptions) => Promise<Redis>;\n\n  constructor(opts: ClusterConnectOptions) {\n    this.#startupNodes = opts.nodes.map((node) =>\n      new ClusterNode(node.hostname, node.port ?? 6379)\n    );\n    this.#maxConnections = opts.maxConnections ?? 50; // TODO(uki00a): To be honest, I'm not sure if this default value is appropriate...\n    this.#newRedis = opts.newRedis ?? connect;\n  }\n\n  get connection(): Connection {\n    throw new NotImplementedError(\"Not implemented yet\");\n  }\n\n  exec(command: string, ...args: RedisValue[]): Promise<RedisReply> {\n    return this.sendCommand(command, args);\n  }\n\n  async sendCommand(\n    command: string,\n    _args?: RedisValue[],\n    options?: SendCommandOptions,\n  ): Promise<RedisReply> {\n    if (this.#refreshTableASAP) {\n      await this.initializeSlotsCache();\n    }\n    let asking = false;\n    let askingNode: ClusterNode | null = null;\n    let tryRandomNode = false;\n    let ttl = kRedisClusterRequestTTL;\n    let lastError: null | Error;\n    const args = _args ?? [];\n    while (ttl > 0) {\n      ttl -= 1;\n      const key = getKeyFromCommand(command, args);\n      if (key == null) {\n        throw new ClusterError(\n          \"No way to dispatch this command to Redis Cluster.\",\n        );\n      }\n      const slot = calculateSlot(key);\n      let r: Redis;\n      if (asking && askingNode) {\n        r = await this.#getConnectionByNode(askingNode);\n        askingNode = null;\n      } else if (tryRandomNode) {\n        r = await this.#getRandomConnection();\n        tryRandomNode = false;\n      } else {\n        r = await this.#getConnectionBySlot(slot);\n      }\n\n      try {\n        if (asking) {\n          await r.asking();\n        }\n        asking = false;\n        const reply = await r.sendCommand(command, args, options);\n        return reply;\n      } catch (err) {\n        if (err instanceof Error) {\n          lastError = err;\n        } else {\n          throw err; // An unexpected error occurred.\n        }\n\n        if (err instanceof Deno.errors.BadResource) {\n          tryRandomNode = true;\n          if (ttl < kRedisClusterRequestTTL / 2) {\n            await delay(100);\n          }\n          continue;\n        } else if (err instanceof ErrorReplyError) {\n          const [code, newSlot, ipAndPort] = err.message.split(/\\s+/);\n          if (code === \"-MOVED\" || code === \"-ASK\") {\n            if (code === \"-ASK\") {\n              asking = true;\n            } else {\n              // Server replied with MOVED. It's better for us to\n              // ask for CLUSTER NODES the next time.\n              this.#refreshTableASAP = true;\n            }\n            const node = ClusterNode.parseIPAndPort(ipAndPort);\n            if (asking) {\n              // Server replied with -ASK. We should send the next query to the redirected node.\n              askingNode = node;\n            } else {\n              this.#nodeBySlot[parseInt(newSlot)] = node;\n            }\n          } else {\n            throw err;\n          }\n        } else {\n          throw err; // An unexpected error occurred.\n        }\n      }\n    }\n    throw new ClusterError(\n      `Too many Cluster redirections? (last error: ${\n        lastError!.message ??\n          \"\"\n      })`,\n    );\n  }\n\n  subscribe<TMessage extends PubSubMessageType = DefaultPubSubMessageType>(\n    _command: SubscribeCommand,\n    ..._channelsOrPatterns: Array<string>\n  ): Promise<RedisSubscription<TMessage>> {\n    return Promise.reject(new NotImplementedError(\"ClusterClient#subscribe\"));\n  }\n\n  close(): void {\n    const nodeNames = Object.keys(this.#connectionByNodeName);\n    for (const nodeName of nodeNames) {\n      const conn = this.#connectionByNodeName[nodeName];\n      if (conn) {\n        conn.close();\n        delete this.#connectionByNodeName[nodeName];\n      }\n    }\n    this.#refreshTableASAP = true;\n  }\n\n  async initializeSlotsCache(): Promise<void> {\n    for (const node of this.#startupNodes) {\n      try {\n        const redis = await this.#getRedisLink(node);\n        try {\n          const clusterSlots = await redis.clusterSlots() as Array<\n            [number, number, [string, number]]\n          >;\n          const nodes = [] as ClusterNode[];\n          const slotMap = {} as SlotMap;\n          for (const [from, to, master] of clusterSlots) {\n            for (let slot = from; slot <= to; slot++) {\n              const [ip, port] = master;\n              const node = new ClusterNode(ip, port);\n              nodes.push(node);\n              slotMap[slot] = node;\n            }\n          }\n          this.#nodeBySlot = slotMap;\n          await this.#populateStartupNodes(nodes);\n          this.#refreshTableASAP = false;\n          return;\n        } finally {\n          await redis.quit();\n        }\n      } catch (_err) {\n        // TODO: Consider logging `_err` here\n        continue;\n      }\n    }\n  }\n\n  #populateStartupNodes(nodes: ClusterNode[]) {\n    for (const node of nodes) {\n      this.#startupNodes.push(node);\n    }\n\n    this.#startupNodes = distinctBy(\n      this.#startupNodes,\n      (node: ClusterNode) => node.name,\n    );\n  }\n\n  async #getRandomConnection(): Promise<Redis> {\n    for (const node of shuffle(this.#startupNodes)) {\n      try {\n        let conn = this.#connectionByNodeName[node.name];\n        if (conn) {\n          const message = await conn.ping();\n          if (message === \"PONG\") {\n            return conn;\n          }\n        } else {\n          conn = await this.#getRedisLink(node);\n          try {\n            const message = await conn.ping();\n            if (message === \"PONG\") {\n              await this.#closeExistingConnection();\n              this.#connectionByNodeName[node.name] = conn;\n              return conn;\n            } else {\n              await conn.quit();\n            }\n          } catch {\n            conn.close();\n          }\n        }\n      } catch {\n        // Just try with the next node.\n      }\n    }\n    throw new ClusterError(\"Can't reach a single startup node.\");\n  }\n\n  #getConnectionBySlot(slot: number): Promise<Redis> {\n    const node = this.#nodeBySlot[slot];\n    if (node) {\n      return this.#getConnectionByNode(node);\n    } else {\n      return this.#getRandomConnection();\n    }\n  }\n\n  async #getConnectionByNode(node: ClusterNode): Promise<Redis> {\n    let conn = this.#connectionByNodeName[node.name];\n    if (conn) {\n      return conn;\n    } else {\n      try {\n        await this.#closeExistingConnection();\n        conn = await this.#getRedisLink(node);\n        this.#connectionByNodeName[node.name] = conn;\n        return conn;\n      } catch {\n        return this.#getRandomConnection();\n      }\n    }\n  }\n\n  async #closeExistingConnection() {\n    const nodeNames = Object.keys(this.#connectionByNodeName);\n    while (nodeNames.length >= this.#maxConnections) {\n      const nodeName = sample(nodeNames)!;\n      const conn = this.#connectionByNodeName[nodeName];\n      delete this.#connectionByNodeName[nodeName];\n      try {\n        await conn.quit();\n      } catch (err) {\n        // deno-lint-ignore no-console -- TODO: consider improving logging\n        console.error(err);\n      }\n    }\n  }\n\n  #getRedisLink(node: ClusterNode): Promise<Redis> {\n    const { hostname, port } = node;\n    return this.#newRedis({ hostname, port });\n  }\n}\n\nfunction getKeyFromCommand(command: string, args: RedisValue[]): string | null {\n  switch (command.toLowerCase()) {\n    case \"info\":\n    case \"multi\":\n    case \"exec\":\n    case \"slaveof\":\n    case \"config\":\n    case \"shutdown\":\n      return null;\n    default:\n      return args[0] as string;\n  }\n}\n\n/**\n * Connects to the Redis Cluster.\n *\n * @see https://redis.io/topics/cluster-tutorial\n * @see https://redis.io/topics/cluster-spec\n */\nasync function connectToCluster(opts: ClusterConnectOptions): Promise<Redis> {\n  const client = new ClusterClient(opts);\n  await client.initializeSlotsCache();\n  return create(client);\n}\n\nexport { connectToCluster as connect };\n"
  },
  {
    "path": "experimental/pool/mod.ts",
    "content": "export * from \"../../pool/mod.ts\";\n"
  },
  {
    "path": "experimental/web_streams_connection/mod.ts",
    "content": "/**\n * @module\n * @experimental **NOTE**: This is an unstable module.\n */\nimport { kUnstableCreateProtocol } from \"../../internal/symbols.ts\";\nimport type { Redis, RedisConnectOptions } from \"../../redis.ts\";\nimport { connect as _connect } from \"../../redis.ts\";\nimport { Protocol } from \"../../protocol/web_streams/mod.ts\";\n\nfunction createProtocol(conn: Deno.Conn) {\n  return new Protocol(conn);\n}\n\nexport function connect(options: RedisConnectOptions): Promise<Redis> {\n  return _connect({\n    ...options,\n    [kUnstableCreateProtocol]: createProtocol,\n  });\n}\n"
  },
  {
    "path": "import_map.dev.json",
    "content": "{\n  \"imports\": {\n    \"benny\": \"npm:benny@3.7.1\",\n    \"@uki00a/deno-json-lint\": \"jsr:@uki00a/deno-json-lint@^0.4.0\"\n  }\n}\n"
  },
  {
    "path": "import_map.test.json",
    "content": "{\n  \"imports\": {\n    \"https://deno.land/x/redis/mod.ts\": \"./mod.ts\",\n    \"https://deno.land/x/redis/experimental/cluster/mod.ts\": \"./experimental/cluster/mod.ts\"\n  }\n}\n"
  },
  {
    "path": "internal/buffered_readable_stream.ts",
    "content": "import { concateBytes } from \"./concate_bytes.ts\";\n\nconst LF = \"\\n\".charCodeAt(0);\n/**\n * Wraps `ReadableStream` to provide buffering. Heavily inspired by `deno_std/io/buf_reader.ts`.\n *\n * {@link https://github.com/denoland/deno_std/blob/0.204.0/io/buf_reader.ts}\n */\nexport class BufferedReadableStream {\n  #reader: ReadableStreamBYOBReader;\n  #buffer: Uint8Array;\n  constructor(readable: ReadableStream<Uint8Array>) {\n    this.#reader = readable.getReader({ mode: \"byob\" });\n    this.#buffer = new Uint8Array(0);\n  }\n\n  async readLine(): Promise<Uint8Array> {\n    const i = this.#buffer.indexOf(LF);\n    if (i > -1) {\n      return this.#consume(i + 1);\n    }\n    for (;;) {\n      await this.#fill();\n      const i = this.#buffer.indexOf(LF);\n      if (i > -1) return this.#consume(i + 1);\n    }\n  }\n\n  async readN(n: number): Promise<Uint8Array> {\n    if (n <= this.#buffer.length) {\n      return this.#consume(n);\n    }\n\n    if (n === 0) {\n      return new Uint8Array(0);\n    }\n\n    if (this.#buffer.length === 0) {\n      const buffer = new Uint8Array(n);\n      const { done, value } = await this.#reader.read(buffer, {\n        min: buffer.length,\n      });\n      if (done) {\n        throw new Deno.errors.BadResource();\n      }\n      return value;\n    } else {\n      const remaining = n - this.#buffer.length;\n      const buffer = new Uint8Array(remaining);\n      const { value, done } = await this.#reader.read(buffer, {\n        min: remaining,\n      });\n      if (done) {\n        throw new Deno.errors.BadResource();\n      }\n\n      const result = concateBytes(this.#buffer, value);\n      this.#buffer = new Uint8Array();\n      return result;\n    }\n  }\n\n  #consume(n: number): Uint8Array {\n    const b = this.#buffer.subarray(0, n);\n    this.#buffer = this.#buffer.subarray(n);\n    return b;\n  }\n\n  async #fill() {\n    const chunk = await this.#reader.read(new Uint8Array(1024));\n    if (chunk.done) {\n      throw new Deno.errors.BadResource();\n    }\n    const bytes = chunk.value;\n    this.#buffer = concateBytes(this.#buffer, bytes);\n  }\n}\n"
  },
  {
    "path": "internal/buffered_readable_stream_test.ts",
    "content": "import { encoder } from \"./encoding.ts\";\nimport { assertEquals, assertRejects } from \"../deps/std/assert.ts\";\nimport { BufferedReadableStream } from \"./buffered_readable_stream.ts\";\n\nDeno.test({\n  name: \"BufferedReadableStream\",\n  permissions: \"none\",\n  fn: async (t) => {\n    const decoder = new TextDecoder();\n    await t.step(\"readLine\", async () => {\n      const readable = createReadableStreamFromString(\n        \"*2\\r\\n$5\\r\\nhello\\r\\n:1234\\r\\n\",\n      );\n      const buffered = new BufferedReadableStream(readable);\n      assertEquals(decoder.decode(await buffered.readLine()), \"*2\\r\\n\");\n      assertEquals(decoder.decode(await buffered.readLine()), \"$5\\r\\n\");\n      assertEquals(decoder.decode(await buffered.readLine()), \"hello\\r\\n\");\n      assertEquals(decoder.decode(await buffered.readLine()), \":1234\\r\\n\");\n      await assertRejects(() => buffered.readLine(), Deno.errors.BadResource);\n    });\n\n    await t.step(\"readN\", async () => {\n      const readable = createReadableStreamFromString(\n        \"$12\\r\\nhello_world!\\r\\n\",\n      );\n      const buffered = new BufferedReadableStream(readable);\n\n      await buffered.readN(0);\n      assertEquals(decoder.decode(await buffered.readLine()), \"$12\\r\\n\");\n\n      {\n        const buf = await buffered.readN(5);\n        assertEquals(decoder.decode(buf), \"hello\");\n      }\n\n      await buffered.readN(0);\n\n      {\n        const buf = await buffered.readN(7);\n        assertEquals(decoder.decode(buf), \"_world!\");\n      }\n\n      await buffered.readN(0);\n\n      {\n        const buf = await buffered.readN(2);\n        assertEquals(decoder.decode(buf), \"\\r\\n\");\n      }\n\n      await buffered.readN(0);\n      await assertRejects(\n        () => buffered.readN(1),\n        Deno.errors.BadResource,\n      );\n    });\n\n    await t.step(\n      \"`readN` should not throw `RangeError: offset is out of bounds` error\",\n      async () => {\n        const readable = new ReadableStream({\n          type: \"bytes\",\n          start(controller) {\n            controller.enqueue(encoder.encode(\"foobar\"));\n            controller.close();\n          },\n        });\n        const buffered = new BufferedReadableStream(readable);\n        {\n          const buf = await buffered.readN(3);\n          assertEquals(decoder.decode(buf), \"foo\");\n        }\n\n        {\n          const buf = await buffered.readN(1);\n          assertEquals(decoder.decode(buf), \"b\");\n        }\n\n        await buffered.readN(0);\n        {\n          const buf = await buffered.readN(2);\n          assertEquals(decoder.decode(buf), \"ar\");\n        }\n      },\n    );\n  },\n});\n\nfunction createReadableStreamFromString(s: string): ReadableStream<Uint8Array> {\n  const encoder = new TextEncoder();\n  let numRead = 0;\n  return new ReadableStream({\n    type: \"bytes\",\n    pull(controller) {\n      controller.enqueue(encoder.encode(s[numRead]));\n      numRead++;\n      if (numRead >= s.length) {\n        controller.close();\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "internal/concate_bytes.ts",
    "content": "export function concateBytes(a: Uint8Array, b: Uint8Array) {\n  if (a.length === 0) return b;\n  const size = a.length + b.length;\n  const buf = new Uint8Array(size);\n  buf.set(a);\n  buf.set(b, a.length);\n  return buf;\n}\n"
  },
  {
    "path": "internal/concate_bytes_test.ts",
    "content": "import { assertEquals } from \"../deps/std/assert.ts\";\nimport { concateBytes } from \"./concate_bytes.ts\";\n\nDeno.test(\"concateBytes\", () => {\n  const encoder = new TextEncoder();\n  const decoder = new TextDecoder();\n  const e = (s: string) => encoder.encode(s);\n  const d = (b: Uint8Array) => decoder.decode(b);\n\n  assertEquals(d(concateBytes(e(\"foo\"), e(\"bar\"))), \"foobar\");\n  assertEquals(d(concateBytes(e(\"\"), e(\"\"))), \"\");\n  assertEquals(d(concateBytes(e(\"\"), e(\"hello\"))), \"hello\");\n});\n"
  },
  {
    "path": "internal/delegate.ts",
    "content": "type Delegate<TObject, TMethods extends keyof TObject> =\n  Pick<TObject, TMethods> extends Record<\n    string | symbol,\n    // deno-lint-ignore ban-types -- This is used only as a constraint on the type argument\n    Function\n  > ? Pick<TObject, TMethods>\n    : never;\n\nexport function delegate<\n  TObject extends object,\n  TMethods extends keyof TObject,\n>(\n  target: TObject,\n  methods: Array<TMethods>,\n): Delegate<TObject, TMethods> {\n  return methods.reduce((proxy, method) => {\n    if (typeof target[method] === \"function\") {\n      proxy[method] = target[method].bind(target);\n    } else {\n      throw new Error(`${String(method)} should be a method`);\n    }\n    return proxy;\n  }, {} as Delegate<TObject, TMethods>);\n}\n"
  },
  {
    "path": "internal/delegate_test.ts",
    "content": "import { delegate } from \"./delegate.ts\";\nimport {\n  assert,\n  assertExists,\n  assertFalse,\n  assertNotStrictEquals,\n} from \"../deps/std/assert.ts\";\nimport type { Has, IsExact, NotHas } from \"../deps/std/testing.ts\";\nimport { assertType } from \"../deps/std/testing.ts\";\n\nDeno.test(\"delegate\", () => {\n  class Connection {\n    #isConnected = false;\n    connect(): void {\n      this.#isConnected = true;\n    }\n    close(): void {\n      this.#isConnected = false;\n    }\n    isConnected(): boolean {\n      return this.#isConnected;\n    }\n    isClosed(): boolean {\n      return !this.#isConnected;\n    }\n  }\n  const base = new Connection();\n  const proxy = delegate(base, [\"connect\", \"isConnected\"]);\n  assertNotStrictEquals(proxy.connect, base.connect);\n  assertNotStrictEquals(proxy.isConnected, base.isConnected);\n\n  const kClose = \"close\";\n  assert(\n    // @ts-expect-error - `close()` should not be defined.\n    proxy[kClose] === undefined,\n  );\n  assertExists(base[kClose]);\n\n  assertType<Has<Connection, typeof proxy>>(true);\n  assertType<NotHas<typeof proxy, Connection>>(true);\n  assertType<IsExact<Connection[\"connect\"], typeof proxy[\"connect\"]>>(true);\n  assertType<IsExact<Connection[\"isConnected\"], typeof proxy[\"isConnected\"]>>(\n    true,\n  );\n\n  assertFalse(base.isConnected());\n  assertFalse(proxy.isConnected());\n  proxy.connect();\n  assert(base.isConnected());\n  assert(proxy.isConnected());\n});\n"
  },
  {
    "path": "internal/encoding.ts",
    "content": "export const encoder = new TextEncoder();\nexport const decoder = new TextDecoder();\n"
  },
  {
    "path": "internal/on.ts",
    "content": "interface Options {\n  signal?: AbortSignal;\n}\n/**\n * Converts {@linkcode EventTarget} to {@linkcode AsyncIterableIterator}, similar to `on()` in `node:events`.\n */\nexport function on(\n  eventTarget: EventTarget,\n  event: string,\n  options: Options = {},\n): AsyncIterableIterator<Event> {\n  // TODO: Optimize the implementation.\n  const abortController = new AbortController();\n  const signal = options.signal\n    ? AbortSignal.any([options.signal, abortController.signal])\n    : abortController.signal;\n  const readerQueue: Array<PromiseWithResolvers<Event>> = [];\n  const bufferedEventQueue: Array<Event> = [];\n  if (!signal.aborted) {\n    eventTarget.addEventListener(\n      event,\n      (event) => {\n        if (readerQueue.length) {\n          const { resolve } = readerQueue.shift()!;\n          resolve(event);\n        } else {\n          bufferedEventQueue.push(event);\n        }\n      },\n      { signal },\n    );\n  }\n  function cleanup(): void {\n    for (const d of readerQueue) {\n      d.reject(signal.reason);\n    }\n    readerQueue.length = 0;\n  }\n  const iter: AsyncIterableIterator<Event> = {\n    [Symbol.asyncIterator]() {\n      return this;\n    },\n    async next() {\n      if (signal.aborted) {\n        return { done: true, value: undefined };\n      } else if (bufferedEventQueue.length) {\n        const event = bufferedEventQueue.shift()!;\n        return { done: false, value: event };\n      } else {\n        const deferred = Promise.withResolvers<Event>();\n        readerQueue.push(deferred);\n        const value = await deferred.promise;\n        return { done: false, value };\n      }\n    },\n    return() {\n      abortController.abort();\n      cleanup();\n      return Promise.resolve({ done: true, value: undefined });\n    },\n  };\n  return iter;\n}\n"
  },
  {
    "path": "internal/on_test.ts",
    "content": "import { on } from \"./on.ts\";\nimport {\n  assert,\n  assertEquals,\n  assertStrictEquals,\n} from \"../deps/std/assert.ts\";\n\nDeno.test({\n  name: \"on\",\n  permissions: \"none\",\n  fn: async (t) => {\n    await t.step(\"implements [Symbol.asyncIterator]\", async () => {\n      const eventType = \"foo\";\n      const target = new EventTarget();\n      const ac = new AbortController();\n      const iter = on(target, eventType, { signal: ac.signal });\n      const events: Array<Event> = [];\n      const promise = (async () => {\n        for await (const event of iter) {\n          assertStrictEquals(event.type, eventType);\n          events.push(event);\n          if (events.length > 2) {\n            ac.abort();\n          }\n        }\n      })();\n      target.dispatchEvent(new CustomEvent(eventType));\n      target.dispatchEvent(new CustomEvent(eventType + \"bar\"));\n      target.dispatchEvent(new CustomEvent(eventType));\n      target.dispatchEvent(new CustomEvent(eventType));\n      await promise;\n      assertEquals(await iter.next(), { done: true, value: undefined });\n      assertStrictEquals(events.length, 3);\n    });\n\n    await t.step(\"implements Symbol.asyncIterator#return()\", async () => {\n      const eventType = \"bar\";\n      const target = new EventTarget();\n      const ac = new AbortController();\n      const iter = on(target, eventType, { signal: ac.signal });\n\n      target.dispatchEvent(new CustomEvent(eventType));\n      const result = await iter.next();\n      assertStrictEquals(result.done, false);\n      assertStrictEquals(result.value.type, eventType);\n\n      assert(iter.return != null);\n      iter.return();\n\n      assertEquals(await iter.next(), { done: true, value: undefined });\n      target.dispatchEvent(new CustomEvent(eventType));\n      assertEquals(await iter.next(), { done: true, value: undefined });\n    });\n  },\n});\n"
  },
  {
    "path": "internal/symbols.ts",
    "content": "/**\n * @private\n */\nexport const kUnstableReadReply = Symbol(\"deno-redis.readReply\");\n\n/**\n * @private\n */\nexport const kUnstableWriteCommand = Symbol(\"deno-redis.writeCommand\");\n\n/**\n * @private\n */\nexport const kUnstablePipeline = Symbol(\"deno-redis.pipeline\");\n\n/**\n * @private\n */\nexport const kUnstableCreateProtocol = Symbol(\"deno-redis.createProtocol\");\n\n/**\n * @private\n */\nexport const kUnstableProtover = Symbol(\"deno-redis.protover\");\n\n/**\n * @private\n */\nexport const kUnstableStartReadLoop = Symbol(\"deno-redis.startReadLoop\");\n"
  },
  {
    "path": "internal/typed_event_target.ts",
    "content": "export interface TypedEventTarget<TEventMap extends Record<string, unknown>>\n  extends\n    Omit<\n      EventTarget,\n      \"addEventListener\" | \"removeEventListener\" | \"dispatchEvent\"\n    > {\n  addEventListener<K extends keyof TEventMap>(\n    type: K,\n    callback: (\n      event: CustomEvent<TEventMap[K]>,\n    ) => void,\n    options?: AddEventListenerOptions | boolean,\n  ): void;\n\n  removeEventListener<K extends keyof TEventMap>(\n    type: K,\n    callback: (\n      event: CustomEvent<TEventMap[K]>,\n    ) => void,\n    options?: EventListenerOptions | boolean,\n  ): void;\n}\n\nexport function createTypedEventTarget<\n  TEventMap extends Record<string, unknown>,\n>(): TypedEventTarget<TEventMap> {\n  return new EventTarget() as TypedEventTarget<TEventMap>;\n}\n\nexport function dispatchEvent<\n  TEventMap extends Record<string, unknown>,\n  TKey extends Extract<keyof TEventMap, string>,\n>(\n  eventTarget: TypedEventTarget<TEventMap>,\n  event: TKey,\n  detail: TEventMap[TKey],\n): boolean {\n  return (eventTarget as EventTarget).dispatchEvent(\n    new CustomEvent(event, {\n      detail,\n    }),\n  );\n}\n"
  },
  {
    "path": "mod.ts",
    "content": "// Generated by tools/make_mod.ts. Don't edit.\nexport { okReply } from \"./protocol/shared/types.ts\";\nexport { connect, create, createLazyClient, parseURL } from \"./redis.ts\";\nexport {\n  ConnectionClosedError,\n  EOFError,\n  ErrorReplyError,\n  InvalidStateError,\n  NotImplementedError,\n  SubscriptionClosedError,\n} from \"./errors.ts\";\nexport type { Backoff, ExponentialBackoffOptions } from \"./backoff.ts\";\nexport type {\n  ACLLogMode,\n  BitfieldOpts,\n  BitfieldWithOverflowOpts,\n  ClientCachingMode,\n  ClientKillOpts,\n  ClientListOpts,\n  ClientPauseMode,\n  ClientTrackingOpts,\n  ClientType,\n  ClientUnblockingBehaviour,\n  ClusterFailoverMode,\n  ClusterResetMode,\n  ClusterSetSlotSubcommand,\n  GeoRadiusOpts,\n  GeoUnit,\n  HelloOpts,\n  HScanOpts,\n  LInsertLocation,\n  LPosOpts,\n  LPosWithCountOpts,\n  MemoryUsageOpts,\n  MigrateOpts,\n  RedisCommands,\n  RestoreOpts,\n  ScanOpts,\n  ScriptDebugMode,\n  SetOpts,\n  SetReply,\n  SetWithModeOpts,\n  ShutdownMode,\n  SortOpts,\n  SortWithDestinationOpts,\n  SScanOpts,\n  StralgoAlgorithm,\n  StralgoOpts,\n  StralgoTarget,\n  ZAddOpts,\n  ZAddReply,\n  ZInterOpts,\n  ZInterstoreOpts,\n  ZRangeByLexOpts,\n  ZRangeByScoreOpts,\n  ZRangeOpts,\n  ZScanOpts,\n  ZUnionstoreOpts,\n} from \"./command.ts\";\nexport type {\n  Connection,\n  RedisConnectionOptions,\n  SendCommandOptions,\n} from \"./connection.ts\";\nexport type {\n  ConnectionErrorEventDetails,\n  ConnectionEvent,\n  ConnectionEventType,\n  ConnectionReconnectingEventDetails,\n} from \"./events.ts\";\nexport type { Client } from \"./client.ts\";\nexport type { RedisPubSubMessage, RedisSubscription } from \"./subscription.ts\";\nexport type { CommandExecutor } from \"./executor.ts\";\nexport type { RedisPipeline } from \"./pipeline.ts\";\nexport type {\n  Binary,\n  Bulk,\n  BulkNil,\n  BulkString,\n  ConditionalArray,\n  Integer,\n  Raw,\n  RawOrError,\n  RedisReply,\n  RedisValue,\n  SimpleString,\n} from \"./protocol/shared/types.ts\";\nexport type { Redis, RedisConnectOptions } from \"./redis.ts\";\nexport type {\n  StartEndCount,\n  XAddFieldValues,\n  XClaimJustXId,\n  XClaimMessages,\n  XClaimOpts,\n  XClaimReply,\n  XConsumerDetail,\n  XGroupDetail,\n  XId,\n  XIdAdd,\n  XIdCreateGroup,\n  XIdGroupRead,\n  XIdInput,\n  XIdNeg,\n  XIdPos,\n  XInfoConsumer,\n  XInfoConsumersReply,\n  XInfoGroup,\n  XInfoGroupsReply,\n  XInfoStreamFullReply,\n  XInfoStreamReply,\n  XKeyId,\n  XKeyIdGroup,\n  XKeyIdGroupLike,\n  XKeyIdLike,\n  XMaxlen,\n  XMessage,\n  XPendingConsumer,\n  XPendingCount,\n  XPendingReply,\n  XReadGroupOpts,\n  XReadIdData,\n  XReadOpts,\n  XReadReply,\n  XReadReplyRaw,\n  XReadStream,\n  XReadStreamRaw,\n} from \"./stream.ts\";\n"
  },
  {
    "path": "pipeline.ts",
    "content": "import type { Connection, SendCommandOptions } from \"./connection.ts\";\nimport { kEmptyRedisArgs } from \"./protocol/shared/command.ts\";\nimport type { Client } from \"./client.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisSubscription,\n  SubscribeCommand,\n} from \"./subscription.ts\";\nimport type {\n  RawOrError,\n  RedisReply,\n  RedisValue,\n} from \"./protocol/shared/types.ts\";\nimport { okReply } from \"./protocol/shared/types.ts\";\nimport type { Redis } from \"./redis.ts\";\nimport { create } from \"./redis.ts\";\nimport { kUnstablePipeline } from \"./internal/symbols.ts\";\n\nexport interface RedisPipeline extends Redis {\n  flush(): Promise<RawOrError[]>;\n}\n\nexport function createRedisPipeline(\n  connection: Connection,\n  tx = false,\n): RedisPipeline {\n  const pipeline = new PipelineClient(connection, tx);\n  function flush(): Promise<RawOrError[]> {\n    return pipeline.flush();\n  }\n  const client = create(pipeline);\n  return Object.assign(client, { flush });\n}\n\nclass PipelineClient implements Client {\n  private commands: {\n    command: string;\n    args: RedisValue[];\n    returnUint8Arrays?: boolean;\n  }[] = [];\n  private queue: {\n    commands: {\n      command: string;\n      args: RedisValue[];\n      returnUint8Arrays?: boolean;\n    }[];\n    resolve: (value: RawOrError[]) => void;\n    reject: (error: unknown) => void;\n  }[] = [];\n\n  constructor(\n    readonly connection: Connection,\n    private tx: boolean,\n  ) {\n  }\n\n  exec(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<RedisReply> {\n    return this.sendCommand(command, args);\n  }\n\n  sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ): Promise<RedisReply> {\n    this.commands.push({\n      command,\n      args: args ?? kEmptyRedisArgs,\n      returnUint8Arrays: options?.returnUint8Arrays,\n    });\n    return Promise.resolve(okReply);\n  }\n\n  close(): void {\n    return this.connection.close();\n  }\n\n  subscribe<TMessage extends PubSubMessageType = DefaultPubSubMessageType>(\n    _command: SubscribeCommand,\n    ..._channelsOrPatterns: Array<string>\n  ): Promise<RedisSubscription<TMessage>> {\n    return Promise.reject(new Error(\"Pub/Sub cannot be used with a pipeline\"));\n  }\n\n  flush(): Promise<RawOrError[]> {\n    if (this.tx) {\n      this.commands.unshift({ command: \"MULTI\", args: [] });\n      this.commands.push({ command: \"EXEC\", args: [] });\n    }\n    const { promise, resolve, reject } = Promise.withResolvers<RawOrError[]>();\n    this.queue.push({ commands: [...this.commands], resolve, reject });\n    if (this.queue.length === 1) {\n      this.dequeue();\n    }\n    this.commands = [];\n    return promise;\n  }\n\n  private dequeue(): void {\n    const [e] = this.queue;\n    if (!e) return;\n    this.connection[kUnstablePipeline](e.commands)\n      .then(e.resolve)\n      .catch(e.reject)\n      .finally(() => {\n        this.queue.shift();\n        this.dequeue();\n      });\n  }\n}\n"
  },
  {
    "path": "pool/client.ts",
    "content": "import type { Connection, SendCommandOptions } from \"../connection.ts\";\nimport type { Pool } from \"./pool.ts\";\nimport type { Client } from \"../client.ts\";\nimport type {\n  DefaultPubSubMessageType,\n  PubSubMessageType,\n  RedisSubscription,\n  SubscribeCommand,\n} from \"../subscription.ts\";\nimport { createDefaultClient } from \"../default_client.ts\";\nimport {\n  kUnstablePipeline,\n  kUnstableReadReply,\n  kUnstableStartReadLoop,\n  kUnstableWriteCommand,\n} from \"../internal/symbols.ts\";\nimport { delegate } from \"../internal/delegate.ts\";\nimport type { RedisReply, RedisValue } from \"../protocol/shared/types.ts\";\n\nexport function createPoolClient(pool: Pool<Connection>): Client {\n  return new PoolClient(pool);\n}\n\nclass PoolClient implements Client {\n  readonly #pool: Pool<Connection>;\n  constructor(pool: Pool<Connection>) {\n    this.#pool = pool;\n  }\n\n  get connection(): Connection {\n    throw new Error(\"PoolClient.connection is not supported\");\n  }\n\n  async exec(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<RedisReply> {\n    const connection = await this.#pool.acquire();\n    try {\n      const client = createDefaultClient(connection);\n      return await client.exec(command, ...args);\n    } finally {\n      this.#pool.release(connection);\n    }\n  }\n\n  async sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ): Promise<RedisReply> {\n    const connection = await this.#pool.acquire();\n    try {\n      const client = createDefaultClient(connection);\n      return await client.sendCommand(command, args, options);\n    } finally {\n      this.#pool.release(connection);\n    }\n  }\n\n  async subscribe<\n    TMessage extends PubSubMessageType = DefaultPubSubMessageType,\n  >(\n    command: SubscribeCommand,\n    ...channelsOrPatterns: Array<string>\n  ): Promise<RedisSubscription<TMessage>> {\n    const connection = await this.#pool.acquire();\n    const client = createDefaultClient(\n      createPoolConnection(this.#pool, connection),\n    );\n    try {\n      const subscription = await client.subscribe<TMessage>(\n        command,\n        ...channelsOrPatterns,\n      );\n      return subscription;\n    } catch (error) {\n      this.#pool.release(connection);\n      throw error;\n    }\n  }\n\n  close(): void {\n    return this.#pool.close();\n  }\n}\n\nfunction createPoolConnection(\n  pool: Pool<Connection>,\n  connection: Connection,\n): Connection {\n  function close(): void {\n    return pool.release(connection);\n  }\n  return {\n    ...delegate(connection, [\n      \"connect\",\n      \"reconnect\",\n      \"sendCommand\",\n      \"addEventListener\",\n      \"removeEventListener\",\n      Symbol.dispose,\n      kUnstableReadReply,\n      kUnstableWriteCommand,\n      kUnstablePipeline,\n      kUnstableStartReadLoop,\n    ]),\n    close,\n    get name() {\n      return connection.name;\n    },\n    get isConnected() {\n      return connection.isConnected;\n    },\n    get isClosed() {\n      return connection.isClosed;\n    },\n  };\n}\n"
  },
  {
    "path": "pool/default_pool.ts",
    "content": "import type { Pool } from \"./pool.ts\";\n\nclass AlreadyRemovedFromPoolError extends Error {\n  constructor() {\n    super(\"This connection has already been removed from the pool.\");\n  }\n}\n\nconst kDefaultTimeout = 5_000;\nclass DefaultPool<T extends Disposable> implements Pool<T> {\n  readonly #idle: Array<T> = [];\n  readonly #connections: Array<T> = [];\n  #connectionCount: number = 0;\n  readonly #deferredQueue: Array<PromiseWithResolvers<T>> = [];\n  readonly #options: Required<PoolOptions<T>>;\n\n  constructor(\n    {\n      maxConnections = 8,\n      acquire,\n    }: PoolOptions<T>,\n  ) {\n    this.#options = {\n      acquire,\n      maxConnections,\n    };\n  }\n\n  async acquire(signal?: AbortSignal): Promise<T> {\n    signal ||= AbortSignal.timeout(kDefaultTimeout);\n    signal.throwIfAborted();\n    if (this.#idle.length > 0) {\n      const conn = this.#idle.shift()!;\n      return Promise.resolve(conn);\n    }\n\n    if (this.#connectionCount < this.#options.maxConnections) {\n      this.#connectionCount++;\n      try {\n        const connection = await this.#options.acquire();\n        this.#connections.push(connection);\n        return connection;\n      } catch (error) {\n        this.#connectionCount--;\n        throw error;\n      }\n    }\n\n    const deferred = Promise.withResolvers<T>();\n    this.#deferredQueue.push(deferred);\n    const { promise, reject } = deferred;\n    const onAbort = () => {\n      const i = this.#deferredQueue.indexOf(deferred);\n      if (i === -1) return;\n      this.#deferredQueue.splice(i, 1);\n      reject(signal.reason);\n    };\n    signal.addEventListener(\"abort\", onAbort, { once: true });\n    return promise;\n  }\n\n  #has(conn: T): boolean {\n    return this.#connections.includes(conn);\n  }\n\n  release(conn: T): void {\n    if (!this.#has(conn)) {\n      throw new AlreadyRemovedFromPoolError();\n    } else if (this.#deferredQueue.length > 0) {\n      const i = this.#deferredQueue.shift()!;\n      i.resolve(conn);\n    } else {\n      this.#idle.push(conn);\n    }\n  }\n\n  close() {\n    const errors: Array<unknown> = [];\n    for (const x of [...this.#connections]) {\n      try {\n        x[Symbol.dispose]();\n      } catch (error) {\n        errors.push(error);\n      }\n    }\n    this.#connections.length = 0;\n    this.#idle.length = 0;\n    if (errors.length > 0) {\n      throw new AggregateError(errors);\n    }\n  }\n}\n\nexport interface PoolOptions<T extends Disposable> {\n  maxConnections?: number;\n  acquire(): Promise<T>;\n}\n\nexport function createDefaultPool<T extends Disposable>(\n  options: PoolOptions<T>,\n): Pool<T> {\n  return new DefaultPool<T>(options);\n}\n"
  },
  {
    "path": "pool/default_pool_test.ts",
    "content": "import { assert, assertEquals, assertRejects } from \"../deps/std/assert.ts\";\nimport { createDefaultPool } from \"./default_pool.ts\";\n\nclass FakeConnection implements Disposable {\n  #isClosed = false;\n  isClosed() {\n    return this.#isClosed;\n  }\n  [Symbol.dispose]() {\n    if (this.#isClosed) {\n      throw new Error(\"Already closed\");\n    }\n    this.#isClosed = true;\n  }\n}\n\nDeno.test({\n  name: \"DefaultPool\",\n  permissions: \"none\",\n  fn: async () => {\n    const openConnections: Array<FakeConnection> = [];\n    const pool = createDefaultPool({\n      acquire: () => {\n        const connection = new FakeConnection();\n        openConnections.push(connection);\n        return Promise.resolve(connection);\n      },\n      maxConnections: 2,\n    });\n    assertEquals(openConnections, []);\n\n    const signal = AbortSignal.timeout(200);\n\n    const conn1 = await pool.acquire(signal);\n    assertEquals(openConnections, [conn1]);\n    assert(openConnections.every((x) => !x.isClosed()));\n    assert(!signal.aborted);\n\n    const conn2 = await pool.acquire(signal);\n    assertEquals(openConnections, [conn1, conn2]);\n    assert(!conn2.isClosed());\n    assert(openConnections.every((x) => !x.isClosed()));\n    assert(!signal.aborted);\n\n    {\n      // Tests timeout handling\n      await assertRejects(\n        () => pool.acquire(signal),\n        \"Intentionally aborted\",\n      );\n      assert(signal.aborted);\n      assertEquals(openConnections, [conn1, conn2]);\n      assert(openConnections.every((x) => !x.isClosed()));\n    }\n\n    {\n      // Tests `release()`\n      pool.release(conn2);\n      assertEquals(openConnections, [conn1, conn2]);\n\n      const conn = await pool.acquire(new AbortController().signal);\n      assert(conn === conn2, \"A new connection should not be created\");\n      assertEquals(openConnections, [conn1, conn2]);\n    }\n\n    {\n      // `Pool#acquire` should wait for an active connection to be released.\n      const signal = AbortSignal.timeout(3_000);\n      const promise = pool.acquire(signal);\n      setTimeout(() => {\n        pool.release(conn1);\n      }, 50);\n      const conn = await promise;\n      assert(conn === conn1, \"A new connection should not be created\");\n      assertEquals(openConnections, [conn1, conn2]);\n      assert(!signal.aborted);\n    }\n\n    {\n      // `Pool#close` closes all connections\n      pool.close();\n      assert(openConnections.every((x) => x.isClosed()));\n    }\n  },\n});\n"
  },
  {
    "path": "pool/mod.ts",
    "content": "import type { Redis, RedisConnectOptions } from \"../redis.ts\";\nimport { create } from \"../redis.ts\";\nimport type { Connection } from \"../connection.ts\";\nimport { createRedisConnection } from \"../default_connection.ts\";\nimport { createDefaultPool } from \"./default_pool.ts\";\nimport { createPoolClient as baseCreatePoolClient } from \"./client.ts\";\n\nexport interface CreatePoolClientOptions {\n  connection: RedisConnectOptions;\n  maxConnections?: number;\n}\n\nexport function createPoolClient(\n  options: CreatePoolClientOptions,\n): Promise<Redis> {\n  const pool = createDefaultPool<Connection>({\n    acquire,\n    maxConnections: options.maxConnections ?? 8,\n  });\n  const client = create(baseCreatePoolClient(pool));\n  return Promise.resolve(client);\n\n  async function acquire(): Promise<Connection> {\n    const { hostname, port, ...connectionOptions } = options.connection;\n    const connection = createRedisConnection(hostname, port, connectionOptions);\n    await connection.connect();\n    return connection;\n  }\n}\n"
  },
  {
    "path": "pool/pool.ts",
    "content": "export interface Pool<T extends Disposable> {\n  acquire(signal?: AbortSignal): Promise<T>;\n  release(item: T): void;\n  close(): void;\n}\n"
  },
  {
    "path": "protocol/deno_streams/command.ts",
    "content": "import type { BufReader, BufWriter } from \"../../deps/std/io.ts\";\nimport { readReply } from \"./reply.ts\";\nimport { ErrorReplyError } from \"../../errors.ts\";\nimport type { RedisReply, RedisValue } from \"../shared/types.ts\";\nimport { encodeCommand } from \"../shared/command.ts\";\nimport type { Command } from \"../shared/protocol.ts\";\n\nexport async function writeCommand(\n  writer: BufWriter,\n  command: string,\n  args: RedisValue[],\n) {\n  const request = encodeCommand(command, args);\n  await writer.write(request);\n}\n\nexport async function sendCommand(\n  writer: BufWriter,\n  reader: BufReader,\n  command: string,\n  args: RedisValue[],\n  returnUint8Arrays?: boolean,\n): Promise<RedisReply> {\n  await writeCommand(writer, command, args);\n  await writer.flush();\n  return readReply(reader, returnUint8Arrays);\n}\n\nexport async function sendCommands(\n  writer: BufWriter,\n  reader: BufReader,\n  commands: Command[],\n): Promise<(RedisReply | ErrorReplyError)[]> {\n  for (const { command, args } of commands) {\n    await writeCommand(writer, command, args);\n  }\n  await writer.flush();\n  const ret: (RedisReply | ErrorReplyError)[] = [];\n  for (let i = 0; i < commands.length; i++) {\n    try {\n      const rep = await readReply(reader, commands[i].returnUint8Arrays);\n      ret.push(rep);\n    } catch (e) {\n      if (e instanceof ErrorReplyError) {\n        ret.push(e);\n      } else {\n        throw e;\n      }\n    }\n  }\n  return ret;\n}\n"
  },
  {
    "path": "protocol/deno_streams/mod.ts",
    "content": "import { BufReader, BufWriter } from \"../../deps/std/io.ts\";\nimport { readReply } from \"./reply.ts\";\nimport { sendCommand, sendCommands, writeCommand } from \"./command.ts\";\n\nimport type { Command, Protocol as BaseProtocol } from \"../shared/protocol.ts\";\nimport type { RedisReply, RedisValue } from \"../shared/types.ts\";\nimport type { ErrorReplyError } from \"../../errors.ts\";\n\nexport class Protocol implements BaseProtocol {\n  #reader: BufReader;\n  #writer: BufWriter;\n\n  constructor(conn: Deno.Conn) {\n    this.#reader = new BufReader(conn);\n    this.#writer = new BufWriter(conn);\n  }\n\n  sendCommand(\n    command: string,\n    args: RedisValue[],\n    returnsUint8Arrays?: boolean | undefined,\n  ): Promise<RedisReply> {\n    return sendCommand(\n      this.#writer,\n      this.#reader,\n      command,\n      args,\n      returnsUint8Arrays,\n    );\n  }\n\n  readReply(returnsUint8Arrays?: boolean): Promise<RedisReply> {\n    return readReply(this.#reader, returnsUint8Arrays);\n  }\n\n  async writeCommand(command: Command): Promise<void> {\n    await writeCommand(this.#writer, command.command, command.args);\n    await this.#writer.flush();\n  }\n\n  pipeline(commands: Command[]): Promise<Array<RedisReply | ErrorReplyError>> {\n    return sendCommands(this.#writer, this.#reader, commands);\n  }\n}\n"
  },
  {
    "path": "protocol/deno_streams/reply.ts",
    "content": "import type { BufReader } from \"../../deps/std/io.ts\";\nimport type * as types from \"../shared/types.ts\";\nimport {\n  ArrayReplyCode,\n  AttributeReplyCode,\n  BigNumberReplyCode,\n  BlobErrorReplyCode,\n  BooleanReplyCode,\n  BulkReplyCode,\n  DoubleReplyCode,\n  ErrorReplyCode,\n  IntegerReplyCode,\n  MapReplyCode,\n  NullReplyCode,\n  PushReplyCode,\n  SetReplyCode,\n  SimpleStringCode,\n  VerbatimStringCode,\n} from \"../shared/reply.ts\";\nimport { EOFError, ErrorReplyError, InvalidStateError } from \"../../errors.ts\";\nimport { decoder } from \"../../internal/encoding.ts\";\n\nexport async function readReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<types.RedisReply> {\n  const res = await reader.peek(1);\n  if (res == null) {\n    throw new EOFError();\n  }\n\n  const code = res[0];\n  if (code === ErrorReplyCode) {\n    await readErrorReplyOrFail(reader);\n  }\n  switch (code) {\n    case IntegerReplyCode:\n      return readIntegerReply(reader);\n    case SimpleStringCode:\n      return readSimpleStringReply(reader, returnUint8Arrays);\n    case BulkReplyCode:\n      return readBulkReply(reader, returnUint8Arrays);\n    case ArrayReplyCode:\n      return readArrayReply(reader, returnUint8Arrays);\n    case MapReplyCode:\n      return readMapReply(reader, returnUint8Arrays);\n    case SetReplyCode:\n      return readSetReply(reader, returnUint8Arrays);\n    case BooleanReplyCode:\n      return readBooleanReply(reader);\n    case DoubleReplyCode:\n      return readDoubleReply(reader, returnUint8Arrays);\n    case BigNumberReplyCode:\n      return readBigNumberReply(reader, returnUint8Arrays);\n    case VerbatimStringCode:\n      return readVerbatimStringReply(reader, returnUint8Arrays);\n    case NullReplyCode:\n      return readNullReply(reader);\n    case PushReplyCode:\n      return readPushReply(reader, returnUint8Arrays);\n    case AttributeReplyCode: {\n      await readAttributeReply(reader);\n      return readReply(reader, returnUint8Arrays);\n    }\n    case BlobErrorReplyCode: {\n      const body = (await readBlobReply(reader, BlobErrorReplyCode)) as string;\n      throw new ErrorReplyError(body);\n    }\n    default:\n      throw new InvalidStateError(\n        `unknown code: '${String.fromCharCode(code)}' (${code})`,\n      );\n  }\n}\n\nasync function readIntegerReply(\n  reader: BufReader,\n): Promise<number> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n\n  return Number.parseInt(decoder.decode(line.subarray(1, line.length)));\n}\n\nfunction readBulkReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<BlobLikeReply> {\n  return readBlobReply(reader, BulkReplyCode, returnUint8Arrays);\n}\n\nfunction readVerbatimStringReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<BlobLikeReply> {\n  return readBlobReply(reader, VerbatimStringCode, returnUint8Arrays);\n}\n\nfunction readSimpleStringReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<string | types.Binary> {\n  return readSingleLineReply(reader, SimpleStringCode, returnUint8Arrays);\n}\n\nfunction readArrayReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<Array<types.RedisReply> | null> {\n  return readArrayLikeReply(reader, returnUint8Arrays);\n}\n\nfunction readPushReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<Array<types.RedisReply> | null> {\n  return readArrayLikeReply(reader, returnUint8Arrays);\n}\n\nasync function readArrayLikeReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<Array<types.RedisReply> | null> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n\n  const argCount = parseSize(line);\n  if (argCount === -1) {\n    // `-1` indicates a null array\n    return null;\n  }\n\n  const array: Array<types.RedisReply> = [];\n  for (let i = 0; i < argCount; i++) {\n    array.push(await readReply(reader, returnUint8Arrays));\n  }\n  return array;\n}\n\n/**\n * NOTE: We treat a set type as an array to keep backward compatibility.\n */\nasync function readSetReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<Array<types.RedisReply> | null> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n\n  const size = parseSize(line);\n  if (size === -1) {\n    // `-1` indicates a null set\n    return null;\n  }\n\n  const set: Array<types.RedisReply> = [];\n  for (let i = 0; i < size; i++) {\n    set.push(await readReply(reader, returnUint8Arrays));\n  }\n  return set;\n}\n\n/**\n * NOTE: We treat a map type as an array to keep backward compatibility.\n */\nasync function readMapReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<Array<types.RedisReply> | null> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n  const numberOfFieldValuePairs = parseSize(line);\n  if (numberOfFieldValuePairs === -1) {\n    return null;\n  }\n\n  const entries: Array<types.RedisReply> = [];\n  for (let i = 0; i < (numberOfFieldValuePairs * 2); i++) {\n    entries.push(await readReply(reader, returnUint8Arrays));\n  }\n  return entries;\n}\n\n/**\n * NOTE: Currently, we simply drop attributes.\n * TODO: Provide a way for users to capture attributes.\n */\nasync function readAttributeReply(\n  reader: BufReader,\n): Promise<void> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n  const numberOfAttributes = parseSize(line);\n  if (numberOfAttributes === -1) {\n    return;\n  }\n\n  for (let i = 0; i < numberOfAttributes; i++) {\n    await readReply(reader); // Reads a key\n    await readReply(reader); // Raads a value\n  }\n}\n\nasync function readBooleanReply(reader: BufReader): Promise<1 | 0> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n  const isTrue = line[1] === 116;\n  return isTrue\n    ? 1 // `#t`\n    : 0; // `#f`\n}\n\nfunction readDoubleReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<string | types.Binary> {\n  return readSingleLineReply(reader, DoubleReplyCode, returnUint8Arrays);\n}\n\nfunction readBigNumberReply(\n  reader: BufReader,\n  returnUint8Arrays?: boolean,\n): Promise<string | types.Binary> {\n  return readSingleLineReply(reader, BigNumberReplyCode, returnUint8Arrays);\n}\n\nasync function readNullReply(reader: BufReader): Promise<null> {\n  // Discards the current line\n  await readLine(reader);\n  return null;\n}\n\nasync function readSingleLineReply(\n  reader: BufReader,\n  expectedCode: number,\n  returnUint8Arrays?: boolean,\n): Promise<string | types.Binary> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n\n  if (line[0] !== expectedCode) {\n    parseErrorReplyOrFail(line);\n  }\n  const body = line.subarray(1);\n  return returnUint8Arrays ? body : decoder.decode(body);\n}\n\ntype BlobLikeReply = string | types.Binary | null;\nasync function readBlobReply(\n  reader: BufReader,\n  expectedCode: number,\n  returnUint8Arrays?: boolean,\n): Promise<BlobLikeReply> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n\n  if (line[0] !== expectedCode) {\n    parseErrorReplyOrFail(line);\n  }\n\n  const size = parseSize(line);\n  if (size < 0) {\n    // nil bulk reply\n    return null;\n  }\n  const dest = new Uint8Array(size + 2);\n  await reader.readFull(dest);\n  const body = dest.subarray(0, dest.length - 2); // Strip CR and LF\n  return returnUint8Arrays ? body : decoder.decode(body);\n}\n\nfunction parseErrorReplyOrFail(line: Uint8Array): never {\n  const code = line[0];\n  if (code === ErrorReplyCode) {\n    throw new ErrorReplyError(decoder.decode(line));\n  }\n  throw new Error(`invalid line: ${line}`);\n}\n\nasync function readErrorReplyOrFail(reader: BufReader): Promise<never> {\n  const line = await readLine(reader);\n  if (line == null) {\n    throw new InvalidStateError();\n  }\n  parseErrorReplyOrFail(line);\n}\n\nasync function readLine(reader: BufReader): Promise<Uint8Array> {\n  let result = await reader.readLine();\n  if (result == null) {\n    throw new InvalidStateError();\n  }\n\n  let line = result.line;\n\n  while (result?.more) {\n    result = await reader.readLine();\n    if (result == null) {\n      throw new InvalidStateError();\n    }\n    const mergedLine = new Uint8Array(line.length + result.line.length);\n    mergedLine.set(line);\n    mergedLine.set(result.line, line.length);\n    line = mergedLine;\n  }\n  return line;\n}\n\nfunction parseSize(line: Uint8Array): number {\n  const sizeStr = line.subarray(1, line.length);\n  const size = parseInt(decoder.decode(sizeStr));\n  return size;\n}\n"
  },
  {
    "path": "protocol/deno_streams/reply_test.ts",
    "content": "import { assertEquals, assertRejects } from \"../../deps/std/assert.ts\";\nimport { BufReader, readerFromStreamReader } from \"../../deps/std/io.ts\";\nimport { ErrorReplyError } from \"../../errors.ts\";\nimport { readReply } from \"./reply.ts\";\nimport type { RedisReply } from \"../shared/types.ts\";\n\nDeno.test({\n  name: \"readReply\",\n  permissions: \"none\",\n  fn: async (t) => {\n    await t.step(\"array\", async () => {\n      const encoder = new TextEncoder();\n      const rawReply = \"*4\\r\\n$3\\r\\nfoo\\r\\n_\\r\\n(123456789\\r\\n:456\\r\\n\";\n      const readable = ReadableStream.from([encoder.encode(rawReply)]);\n      const reader = readable.getReader();\n      const reply = await readReply(\n        BufReader.create(readerFromStreamReader(reader)),\n      );\n      assertEquals(reply, [\"foo\", null, \"123456789\", 456]);\n    });\n\n    await t.step(\"blob error\", async () => {\n      const encoder = new TextEncoder();\n      const rawReply = \"!21\\r\\nSYNTAX invalid syntax\\r\\n_\\r\\n\";\n      const readable = ReadableStream.from([encoder.encode(rawReply)]);\n      const reader = readable.getReader();\n      const buf = BufReader.create(readerFromStreamReader(reader));\n      await assertRejects(\n        () => readReply(buf),\n        ErrorReplyError,\n        \"SYNTAX invalid syntax\",\n      );\n\n      assertEquals(await readReply(buf), null);\n    });\n\n    await t.step(\"attribute\", async () => {\n      const encoder = new TextEncoder();\n      const cases: Array<[given: string, expected: RedisReply]> = [\n        [\n          \"|1\\r\\n+foo\\r\\n%2\\r\\n$3\\r\\nbar\\r\\n:123\\r\\n$1\\r\\na\\r\\n,1.23\\r\\n+Hello World\\r\\n\",\n          \"Hello World\",\n        ],\n        [\n          \"*3\\r\\n:123\\r\\n|2\\r\\n+foo\\r\\n:1\\r\\n+bar\\r\\n:45\\r\\n+str\\r\\n,1.23\\r\\n\",\n          [123, \"str\", \"1.23\"],\n        ],\n      ];\n\n      for (\n        const [given, expected] of cases\n      ) {\n        const readable = ReadableStream.from([encoder.encode(given)]);\n        const reader = readable.getReader();\n        const buf = BufReader.create(readerFromStreamReader(reader));\n        const actual = await readReply(buf);\n        assertEquals(actual, expected);\n      }\n    });\n  },\n});\n"
  },
  {
    "path": "protocol/shared/command.ts",
    "content": "import { concat } from \"../../deps/std/bytes.ts\";\nimport { encoder } from \"../../internal/encoding.ts\";\nimport type { RedisValue } from \"./types.ts\";\nimport type { Command } from \"./protocol.ts\";\n\nconst CRLF = encoder.encode(\"\\r\\n\");\nconst ArrayCode = encoder.encode(\"*\");\nconst BulkCode = encoder.encode(\"$\");\n\nconst kEmptyBuffer = new Uint8Array(0);\nexport const kEmptyRedisArgs: Array<RedisValue> = [];\n\nexport function encodeCommand(\n  command: string,\n  args: RedisValue[],\n): Uint8Array {\n  const encodedArgsCount = encoder.encode(\n    String(1 + args.length),\n  );\n  const encodedCommand = encoder.encode(command);\n  const encodedCommandLength = encoder.encode(\n    String(encodedCommand.byteLength),\n  );\n  let totalBytes = ArrayCode.byteLength +\n    encodedArgsCount.byteLength +\n    CRLF.byteLength +\n    BulkCode.byteLength +\n    encodedCommandLength.byteLength +\n    CRLF.byteLength +\n    encodedCommand.byteLength +\n    CRLF.byteLength;\n  const encodedArgs: Array<Uint8Array> = Array(args.length);\n  for (let i = 0; i < args.length; i++) {\n    const arg = args[i];\n    const bytes = arg instanceof Uint8Array\n      ? arg\n      : (arg == null ? kEmptyBuffer : encoder.encode(String(arg)));\n    const bytesLen = bytes.byteLength;\n    totalBytes += BulkCode.byteLength +\n      String(bytesLen).length +\n      CRLF.byteLength +\n      bytes.byteLength +\n      CRLF.byteLength;\n    encodedArgs[i] = bytes;\n  }\n\n  const request = new Uint8Array(totalBytes);\n  let index = 0;\n  index = writeFrom(request, ArrayCode, index);\n  index = writeFrom(request, encodedArgsCount, index);\n  index = writeFrom(request, CRLF, index);\n  index = writeFrom(request, BulkCode, index);\n  index = writeFrom(request, encodedCommandLength, index);\n  index = writeFrom(request, CRLF, index);\n  index = writeFrom(request, encodedCommand, index);\n  index = writeFrom(request, CRLF, index);\n  for (let i = 0; i < encodedArgs.length; i++) {\n    const encodedArg = encodedArgs[i];\n    const encodedArgLength = encoder.encode(String(encodedArg.byteLength));\n    index = writeFrom(request, BulkCode, index);\n    index = writeFrom(request, encodedArgLength, index);\n    index = writeFrom(request, CRLF, index);\n    index = writeFrom(request, encodedArg, index);\n    index = writeFrom(request, CRLF, index);\n  }\n\n  return request;\n}\n\nfunction writeFrom(\n  bytes: Uint8Array,\n  payload: Uint8Array,\n  fromIndex: number,\n): number {\n  bytes.set(payload, fromIndex);\n  return fromIndex + payload.byteLength;\n}\n\nexport function encodeCommands(commands: Array<Command>): Uint8Array {\n  // TODO: find a more optimized solution.\n  const bufs: Array<Uint8Array> = Array(commands.length);\n  for (let i = 0; i < commands.length; i++) {\n    const { command, args } = commands[i];\n    bufs[i] = encodeCommand(command, args);\n  }\n  return concat(bufs);\n}\n"
  },
  {
    "path": "protocol/shared/command_test.ts",
    "content": "import { encodeCommand } from \"./command.ts\";\nimport { assertEquals } from \"../../deps/std/assert.ts\";\n\nDeno.test({\n  name: \"encodeCommand\",\n  permissions: \"none\",\n  fn: () => {\n    const actual = encodeCommand(\"SET\", [\"name\", \"bar\"]);\n    const expected = new TextEncoder().encode(\n      \"*3\\r\\n$3\\r\\nSET\\r\\n$4\\r\\nname\\r\\n$3\\r\\nbar\\r\\n\",\n    );\n    assertEquals(actual, expected);\n  },\n});\n"
  },
  {
    "path": "protocol/shared/protocol.ts",
    "content": "import type { RedisReply, RedisValue } from \"./types.ts\";\nimport type { ErrorReplyError } from \"../../errors.ts\";\n\nexport interface Command {\n  command: string;\n  args: RedisValue[];\n  returnUint8Arrays?: boolean;\n}\n\nexport interface Protocol {\n  sendCommand(\n    command: string,\n    args: Array<RedisValue>,\n    returnsUint8Arrays?: boolean,\n  ): Promise<RedisReply>;\n  readReply(returnsUint8Array?: boolean): Promise<RedisReply>;\n  writeCommand(command: Command): Promise<void>;\n  pipeline(\n    commands: Array<Command>,\n  ): Promise<Array<RedisReply | ErrorReplyError>>;\n}\n"
  },
  {
    "path": "protocol/shared/reply.ts",
    "content": "/**\n * Represents a number in RESP2/RESP3.\n */\nexport const IntegerReplyCode = \":\".charCodeAt(0);\n/**\n * Represents a double which is introduced in RESP3.\n */\nexport const DoubleReplyCode = \",\".charCodeAt(0);\n/**\n * Represents a blob string in RESP2/RESP3.\n */\nexport const BulkReplyCode = \"$\".charCodeAt(0);\nexport const SimpleStringCode = \"+\".charCodeAt(0);\n/** Represents a verbatim string in RESP3. */\nexport const VerbatimStringCode = \"=\".charCodeAt(0);\nexport const ArrayReplyCode = \"*\".charCodeAt(0);\n/**\n * Represents a simple error in RESP2/RESP3.\n */\nexport const ErrorReplyCode = \"-\".charCodeAt(0);\n/** Represents a blob error which is introduced in RESP3. */\nexport const BlobErrorReplyCode = \"!\".charCodeAt(0);\n/** Represents a map which is introduced in RESP3. */\nexport const MapReplyCode = \"%\".charCodeAt(0);\n/** Represents a set which is introduced in RESP3. */\nexport const SetReplyCode = \"~\".charCodeAt(0);\n/** Represents a boolean which is introduced in RESP3. */\nexport const BooleanReplyCode = \"#\".charCodeAt(0);\n/** Represents a big number which is introduced in RESP3. */\nexport const BigNumberReplyCode = \"(\".charCodeAt(0);\n/** Represents the null type which is introduced in RESP3. */\nexport const NullReplyCode = \"_\".charCodeAt(0);\n/** Represents the attribute type which is introduced in RESP3. */\nexport const AttributeReplyCode = \"|\".charCodeAt(0);\n/** Represents the push type which is introduced in RESP3. */\nexport const PushReplyCode = \">\".charCodeAt(0);\n"
  },
  {
    "path": "protocol/shared/types.ts",
    "content": "import type { ErrorReplyError } from \"../../errors.ts\";\n\n/**\n * @see https://redis.io/topics/protocol\n */\n\nexport type RedisValue = string | number | Uint8Array;\n\n/**\n * @description Represents the type of the value returned by `SimpleStringReply#value()`.\n */\nexport type SimpleString = string;\n\n/**\n * @description Represents the type of the value returned by `IntegerReply#value()`.\n */\nexport type Integer = number;\n\n/**\n * @description Represents the type of the value returned by `BulkReply#value()`.\n */\nexport type Bulk = BulkString | BulkNil;\n\n/**\n * @description Represents the **bulk string** type in the RESP2 protocol.\n */\nexport type BulkString = string;\n\n/**\n * @description Represents the **null bulk string** and **null array** in the RESP2 protocol.\n */\nexport type BulkNil = null;\n\n/**\n * @description Represents the some type in the RESP2 protocol.\n */\nexport type Raw = SimpleString | Integer | Bulk | ConditionalArray | Binary;\n\nexport type Binary = Uint8Array;\n\n/**\n * @description Represents the type of the value returned by `ArrayReply#value()`.\n */\nexport type ConditionalArray = Raw[];\n\nexport type RedisReply = Raw | ConditionalArray;\n\nexport type RawOrError = Raw | ErrorReplyError;\n\nexport const okReply = \"OK\";\n\nexport type Protover = 2 | 3;\n"
  },
  {
    "path": "protocol/web_streams/command.ts",
    "content": "import { readReply } from \"./reply.ts\";\nimport { ErrorReplyError } from \"../../errors.ts\";\nimport type { BufferedReadableStream } from \"../../internal/buffered_readable_stream.ts\";\nimport type { RedisReply, RedisValue } from \"../shared/types.ts\";\nimport { encodeCommand, encodeCommands } from \"../shared/command.ts\";\n\nexport async function writeCommand(\n  writable: WritableStream<Uint8Array>,\n  command: string,\n  args: RedisValue[],\n) {\n  const request = encodeCommand(command, args);\n  const writer = writable.getWriter();\n  try {\n    await writer.write(request);\n  } finally {\n    writer.releaseLock();\n  }\n}\n\nexport async function sendCommand(\n  writable: WritableStream<Uint8Array>,\n  readable: BufferedReadableStream,\n  command: string,\n  args: RedisValue[],\n  returnUint8Arrays?: boolean,\n): Promise<RedisReply> {\n  await writeCommand(writable, command, args);\n  return readReply(readable, returnUint8Arrays);\n}\n\nexport interface Command {\n  command: string;\n  args: RedisValue[];\n  returnUint8Arrays?: boolean;\n}\n\nexport async function sendCommands(\n  writable: WritableStream<Uint8Array>,\n  readable: BufferedReadableStream,\n  commands: Command[],\n): Promise<(RedisReply | ErrorReplyError)[]> {\n  const request = encodeCommands(commands);\n  const writer = writable.getWriter();\n  try {\n    await writer.write(request);\n  } finally {\n    writer.releaseLock();\n  }\n\n  const ret: (RedisReply | ErrorReplyError)[] = [];\n  for (let i = 0; i < commands.length; i++) {\n    try {\n      const rep = await readReply(readable, commands[i].returnUint8Arrays);\n      ret.push(rep);\n    } catch (e) {\n      if (e instanceof ErrorReplyError) {\n        ret.push(e);\n      } else {\n        throw e;\n      }\n    }\n  }\n  return ret;\n}\n"
  },
  {
    "path": "protocol/web_streams/mod.ts",
    "content": "import { sendCommand, sendCommands, writeCommand } from \"./command.ts\";\nimport { readReply } from \"./reply.ts\";\nimport type { Command, Protocol as BaseProtocol } from \"../shared/protocol.ts\";\nimport type { RedisReply, RedisValue } from \"../shared/types.ts\";\nimport type { ErrorReplyError } from \"../../errors.ts\";\nimport { BufferedReadableStream } from \"../../internal/buffered_readable_stream.ts\";\n\nexport class Protocol implements BaseProtocol {\n  #readable: BufferedReadableStream;\n  #writable: WritableStream<Uint8Array>;\n  constructor(conn: Deno.Conn) {\n    this.#readable = new BufferedReadableStream(conn.readable);\n    this.#writable = conn.writable;\n  }\n  sendCommand(\n    command: string,\n    args: RedisValue[],\n    returnsUint8Arrays?: boolean | undefined,\n  ): Promise<RedisReply> {\n    return sendCommand(\n      this.#writable,\n      this.#readable,\n      command,\n      args,\n      returnsUint8Arrays,\n    );\n  }\n\n  readReply(returnsUint8Arrays?: boolean): Promise<RedisReply> {\n    return readReply(this.#readable, returnsUint8Arrays);\n  }\n\n  writeCommand(command: Command): Promise<void> {\n    return writeCommand(this.#writable, command.command, command.args);\n  }\n\n  pipeline(commands: Command[]): Promise<Array<RedisReply | ErrorReplyError>> {\n    return sendCommands(this.#writable, this.#readable, commands);\n  }\n}\n"
  },
  {
    "path": "protocol/web_streams/reply.ts",
    "content": "import type * as types from \"../shared/types.ts\";\nimport {\n  ArrayReplyCode,\n  AttributeReplyCode,\n  BigNumberReplyCode,\n  BlobErrorReplyCode,\n  BooleanReplyCode,\n  BulkReplyCode,\n  DoubleReplyCode,\n  ErrorReplyCode,\n  IntegerReplyCode,\n  MapReplyCode,\n  NullReplyCode,\n  PushReplyCode,\n  SetReplyCode,\n  SimpleStringCode,\n  VerbatimStringCode,\n} from \"../shared/reply.ts\";\nimport { ErrorReplyError, NotImplementedError } from \"../../errors.ts\";\nimport { decoder } from \"../../internal/encoding.ts\";\nimport type { BufferedReadableStream } from \"../../internal/buffered_readable_stream.ts\";\n\nexport async function readReply(\n  readable: BufferedReadableStream,\n  returnUint8Arrays?: boolean,\n) {\n  const line = await readable.readLine();\n  const code = line[0];\n  switch (code) {\n    case ErrorReplyCode: {\n      throw new ErrorReplyError(decoder.decode(line));\n    }\n    case IntegerReplyCode: {\n      return Number.parseInt(decoder.decode(line.subarray(1)));\n    }\n    case SimpleStringCode: {\n      const body = line.slice(1, -2);\n      return returnUint8Arrays ? body : decoder.decode(body);\n    }\n    case BulkReplyCode:\n    case VerbatimStringCode: {\n      const size = Number.parseInt(decoder.decode(line.subarray(1)));\n      if (size < 0) {\n        // nil bulk reply\n        return null;\n      }\n      const buf = await readable.readN(size + 2);\n      const body = buf.subarray(0, size); // Strip CR and LF.\n      return returnUint8Arrays ? body : decoder.decode(body);\n    }\n    case BlobErrorReplyCode: {\n      const size = Number.parseInt(decoder.decode(line.subarray(1)));\n      const buf = await readable.readN(size + 2);\n      const body = buf.subarray(0, size); // Strip CR and LF.\n      throw new ErrorReplyError(decoder.decode(body));\n    }\n    case ArrayReplyCode:\n    case PushReplyCode: {\n      const size = Number.parseInt(decoder.decode(line.slice(1)));\n      if (size === -1) {\n        // `-1` indicates a null array\n        return null;\n      }\n      const array: Array<types.RedisReply> = [];\n      for (let i = 0; i < size; i++) {\n        array.push(await readReply(readable, returnUint8Arrays));\n      }\n      return array;\n    }\n    case MapReplyCode: {\n      // NOTE: We treat a map type as an array to keep backward compatibility.\n      const numberOfFieldValuePairs = Number.parseInt(\n        decoder.decode(line.slice(1)),\n      );\n      if (numberOfFieldValuePairs === -1) {\n        return null;\n      }\n      const entries: Array<types.RedisReply> = [];\n      for (let i = 0; i < (numberOfFieldValuePairs * 2); i++) {\n        entries.push(await readReply(readable, returnUint8Arrays));\n      }\n      return entries;\n    }\n    case SetReplyCode: {\n      // NOTE: We treat a set type as an array to keep backward compatibility.\n      const size = Number.parseInt(decoder.decode(line.slice(1)));\n      if (size === -1) {\n        // `-1` indicates a null set\n        return null;\n      }\n      const set: Array<types.RedisReply> = [];\n      for (let i = 0; i < size; i++) {\n        set.push(await readReply(readable, returnUint8Arrays));\n      }\n      return set;\n    }\n    case BooleanReplyCode: {\n      const isTrue = line[1] === 116;\n      return isTrue\n        ? 1 // `#t`\n        : 0; // `#f`\n    }\n    case BigNumberReplyCode:\n    case DoubleReplyCode: {\n      const body = line.subarray(1, -2);\n      return returnUint8Arrays ? body : decoder.decode(body);\n    }\n    case NullReplyCode: {\n      return null;\n    }\n    case AttributeReplyCode: {\n      // NOTE: Currently, we simply drop attributes.\n      // TODO: Provide a way for users to capture attributes.\n      const numberOfAttributes = Number.parseInt(\n        decoder.decode(line.slice(1)),\n      );\n      if (numberOfAttributes === -1) {\n        return readReply(readable, returnUint8Arrays); // Reads the next reply.\n      }\n      for (let i = 0; i < numberOfAttributes; i++) {\n        await readReply(readable, returnUint8Arrays); // Reads a key\n        await readReply(readable, returnUint8Arrays); // Reads a value\n      }\n\n      return readReply(readable, returnUint8Arrays); // Reads the next reply.\n    }\n    default:\n      throw new NotImplementedError(\n        `'${String.fromCharCode(code)}' reply is not implemented`,\n      );\n  }\n}\n"
  },
  {
    "path": "protocol/web_streams/reply_test.ts",
    "content": "import { assertEquals, assertRejects } from \"../../deps/std/assert.ts\";\nimport { readReply } from \"./reply.ts\";\nimport { BufferedReadableStream } from \"../../internal/buffered_readable_stream.ts\";\nimport { ErrorReplyError } from \"../../errors.ts\";\nimport type { RedisReply } from \"../shared/types.ts\";\n\nDeno.test({\n  name: \"readReply\",\n  permissions: \"none\",\n  fn: async (t) => {\n    await t.step(\"blob\", async () => {\n      const readable = createReadableByteStream(\"$12\\r\\nhello\\nworld!\\r\\n\");\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, \"hello\\nworld!\");\n    });\n\n    await t.step(\"simple string\", async () => {\n      const readable = createReadableByteStream(\"+OK\\r\\n\");\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, \"OK\");\n    });\n\n    await t.step(\"integer\", async () => {\n      const readable = createReadableByteStream(\":1234\\r\\n\");\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, 1234);\n    });\n\n    await t.step(\"array\", async () => {\n      const readable = createReadableByteStream(\n        \"*5\\r\\n$3\\r\\nfoo\\r\\n*2\\r\\n:456\\r\\n+OK\\r\\n_\\r\\n(123456\\r\\n:78\\r\\n\",\n      );\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, [\"foo\", [456, \"OK\"], null, \"123456\", 78]);\n    });\n\n    await t.step(\"map\", async () => {\n      const readable = createReadableByteStream(\n        \"%2\\r\\n+foo\\r\\n:1\\r\\n+bar\\r\\n:2\\r\\n\",\n      );\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, [\"foo\", 1, \"bar\", 2]);\n    });\n\n    await t.step(\"set\", async () => {\n      const readable = createReadableByteStream(\n        \"~3\\r\\n+foo\\r\\n:5\\r\\n$3\\r\\nbar\\r\\n\",\n      );\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, [\"foo\", 5, \"bar\"]);\n    });\n\n    await t.step(\"null\", async () => {\n      const readable = createReadableByteStream(\n        \"_\\r\\n\",\n      );\n      const reply = await readReply(new BufferedReadableStream(readable));\n      assertEquals(reply, null);\n    });\n\n    await t.step(\"blob error\", async () => {\n      const readable = createReadableByteStream(\n        \"!21\\r\\nSYNTAX invalid syntax\\r\\n_\\r\\n\",\n      );\n      const buf = new BufferedReadableStream(readable);\n\n      await assertRejects(\n        () => readReply(buf),\n        ErrorReplyError,\n        \"SYNTAX invalid syntax\",\n      );\n\n      assertEquals(await readReply(buf), null);\n    });\n\n    await t.step(\"attribute\", async () => {\n      const cases: Array<[given: string, expected: RedisReply]> = [\n        [\n          \"|1\\r\\n+foo\\r\\n%2\\r\\n$3\\r\\nbar\\r\\n:123\\r\\n$1\\r\\na\\r\\n,1.23\\r\\n+Hello World\\r\\n\",\n          \"Hello World\",\n        ],\n        [\n          \"*3\\r\\n:123\\r\\n|2\\r\\n+foo\\r\\n:1\\r\\n+bar\\r\\n:45\\r\\n+str\\r\\n,1.23\\r\\n\",\n          [123, \"str\", \"1.23\"],\n        ],\n      ];\n      for (\n        const [given, expected] of cases\n      ) {\n        const readable = createReadableByteStream(given);\n        const buf = new BufferedReadableStream(readable);\n        const actual = await readReply(buf);\n        assertEquals(actual, expected);\n      }\n    });\n  },\n});\n\nfunction createReadableByteStream(payload: string): ReadableStream<Uint8Array> {\n  const encoder = new TextEncoder();\n  let numRead = 0;\n  return new ReadableStream({\n    type: \"bytes\",\n    pull(controller) {\n      if (controller.byobRequest?.view) {\n        const view = controller.byobRequest.view;\n        const buf = new Uint8Array(\n          view.buffer,\n          view.byteOffset,\n          view.byteLength,\n        );\n        const remaining = payload.length - numRead;\n        const written = Math.min(buf.byteLength, remaining);\n        buf.set(encoder.encode(payload.slice(numRead, numRead + written)));\n        numRead += written;\n        controller.byobRequest.respond(written);\n      } else {\n        controller.enqueue(encoder.encode(payload[numRead]));\n        numRead++;\n      }\n      if (numRead >= payload.length) {\n        controller.close();\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "redis.ts",
    "content": "import type {\n  ACLLogMode,\n  BitfieldOpts,\n  BitfieldWithOverflowOpts,\n  ClientCachingMode,\n  ClientKillOpts,\n  ClientListOpts,\n  ClientPauseMode,\n  ClientTrackingOpts,\n  ClientUnblockingBehaviour,\n  ClusterFailoverMode,\n  ClusterResetMode,\n  ClusterSetSlotSubcommand,\n  GeoRadiusOpts,\n  GeoUnit,\n  HelloOpts,\n  HScanOpts,\n  LInsertLocation,\n  LPosOpts,\n  LPosWithCountOpts,\n  MemoryUsageOpts,\n  MigrateOpts,\n  RedisCommands,\n  RestoreOpts,\n  ScanOpts,\n  ScriptDebugMode,\n  SetOpts,\n  SetReply,\n  SetWithModeOpts,\n  ShutdownMode,\n  SortOpts,\n  SortWithDestinationOpts,\n  SScanOpts,\n  StralgoAlgorithm,\n  StralgoOpts,\n  StralgoTarget,\n  ZAddOpts,\n  ZAddReply,\n  ZInterOpts,\n  ZInterstoreOpts,\n  ZRangeByLexOpts,\n  ZRangeByScoreOpts,\n  ZRangeOpts,\n  ZScanOpts,\n  ZUnionstoreOpts,\n} from \"./command.ts\";\nimport { createRedisConnection } from \"./default_connection.ts\";\nimport type { Connection, SendCommandOptions } from \"./connection.ts\";\nimport type { RedisConnectionOptions } from \"./connection.ts\";\nimport type { Client } from \"./client.ts\";\nimport type { RedisSubscription } from \"./subscription.ts\";\nimport { createDefaultClient } from \"./default_client.ts\";\nimport type { ConnectionEventMap, ConnectionEventType } from \"./events.ts\";\nimport type { TypedEventTarget } from \"./internal/typed_event_target.ts\";\nimport type {\n  Binary,\n  Bulk,\n  BulkNil,\n  BulkString,\n  ConditionalArray,\n  Integer,\n  Raw,\n  RedisReply,\n  RedisValue,\n  SimpleString,\n} from \"./protocol/shared/types.ts\";\nimport { createRedisPipeline } from \"./pipeline.ts\";\nimport type {\n  StartEndCount,\n  XAddFieldValues,\n  XClaimJustXId,\n  XClaimMessages,\n  XClaimOpts,\n  XId,\n  XIdAdd,\n  XIdInput,\n  XIdNeg,\n  XIdPos,\n  XKeyId,\n  XKeyIdGroup,\n  XKeyIdGroupLike,\n  XKeyIdLike,\n  XMaxlen,\n  XReadGroupOpts,\n  XReadIdData,\n  XReadOpts,\n  XReadStreamRaw,\n} from \"./stream.ts\";\nimport {\n  convertMap,\n  isCondArray,\n  isNumber,\n  isString,\n  parseXGroupDetail,\n  parseXId,\n  parseXMessage,\n  parseXPendingConsumers,\n  parseXPendingCounts,\n  parseXReadReply,\n  rawnum,\n  rawstr,\n  xidstr,\n} from \"./stream.ts\";\n\nconst binaryCommandOptions = {\n  returnUint8Arrays: true,\n};\n\n/**\n * A high-level client for Redis.\n */\nexport interface Redis\n  extends RedisCommands, TypedEventTarget<ConnectionEventMap> {\n  readonly isClosed: boolean;\n  readonly isConnected: boolean;\n\n  /**\n   * Low level interface for Redis server\n   */\n  sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ): Promise<RedisReply>;\n  connect(): Promise<void>;\n  close(): void;\n\n  [Symbol.dispose](): void;\n}\n\nclass RedisImpl implements Redis {\n  private readonly client: Client;\n\n  get isClosed() {\n    return this.client.connection.isClosed;\n  }\n\n  get isConnected() {\n    return this.client.connection.isConnected;\n  }\n\n  constructor(client: Client) {\n    this.client = client;\n  }\n\n  addEventListener<K extends ConnectionEventType>(\n    type: K,\n    callback: (event: CustomEvent<ConnectionEventMap[K]>) => void,\n    options?: boolean | AddEventListenerOptions,\n  ): void {\n    return this.client.connection.addEventListener(\n      type,\n      callback,\n      options,\n    );\n  }\n\n  removeEventListener<K extends ConnectionEventType>(\n    type: K,\n    callback: (event: CustomEvent<ConnectionEventMap[K]>) => void,\n    options?: boolean | EventListenerOptions,\n  ): void {\n    return this.client.connection.removeEventListener(\n      type,\n      callback,\n      options,\n    );\n  }\n\n  sendCommand(\n    command: string,\n    args?: RedisValue[],\n    options?: SendCommandOptions,\n  ) {\n    return this.client.sendCommand(command, args, options);\n  }\n\n  connect(): Promise<void> {\n    return this.client.connection.connect();\n  }\n\n  close(): void {\n    return this.client.close();\n  }\n\n  [Symbol.dispose](): void {\n    return this.close();\n  }\n\n  async execReply<T extends Raw = Raw>(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<T> {\n    const reply = await this.client.exec(\n      command,\n      ...args,\n    );\n    return reply as T;\n  }\n\n  async execStatusReply(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<SimpleString> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as SimpleString;\n  }\n\n  async execIntegerReply(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<Integer> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as Integer;\n  }\n\n  async execBinaryReply(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<Binary | BulkNil> {\n    const reply = await this.client.sendCommand(\n      command,\n      args,\n      binaryCommandOptions,\n    );\n    return reply as Binary | BulkNil;\n  }\n\n  async execBulkReply<T extends Bulk = Bulk>(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<T> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as T;\n  }\n\n  async execArrayReply<T extends Raw = Raw>(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<T[]> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as Array<T>;\n  }\n\n  async execIntegerOrNilReply(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<Integer | BulkNil> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as Integer | BulkNil;\n  }\n\n  async execStatusOrNilReply(\n    command: string,\n    ...args: RedisValue[]\n  ): Promise<SimpleString | BulkNil> {\n    const reply = await this.client.exec(command, ...args);\n    return reply as SimpleString | BulkNil;\n  }\n\n  aclCat(categoryname?: string) {\n    if (categoryname !== undefined) {\n      return this.execArrayReply<BulkString>(\"ACL\", \"CAT\", categoryname);\n    }\n    return this.execArrayReply<BulkString>(\"ACL\", \"CAT\");\n  }\n\n  aclDelUser(...usernames: string[]) {\n    return this.execIntegerReply(\"ACL\", \"DELUSER\", ...usernames);\n  }\n\n  aclGenPass(bits?: number) {\n    if (bits !== undefined) {\n      return this.execBulkReply<BulkString>(\"ACL\", \"GENPASS\", bits);\n    }\n    return this.execBulkReply<BulkString>(\"ACL\", \"GENPASS\");\n  }\n\n  aclGetUser(username: string) {\n    return this.execArrayReply<BulkString | BulkString[]>(\n      \"ACL\",\n      \"GETUSER\",\n      username,\n    );\n  }\n\n  aclHelp() {\n    return this.execArrayReply<BulkString>(\"ACL\", \"HELP\");\n  }\n\n  aclList() {\n    return this.execArrayReply<BulkString>(\"ACL\", \"LIST\");\n  }\n\n  aclLoad() {\n    return this.execStatusReply(\"ACL\", \"LOAD\");\n  }\n\n  aclLog(count: number): Promise<BulkString[]>;\n  aclLog(mode: ACLLogMode): Promise<SimpleString>;\n  aclLog(param: number | ACLLogMode) {\n    if (param === \"RESET\") {\n      return this.execStatusReply(\"ACL\", \"LOG\", \"RESET\");\n    }\n    return this.execArrayReply<BulkString>(\"ACL\", \"LOG\", param);\n  }\n\n  aclSave() {\n    return this.execStatusReply(\"ACL\", \"SAVE\");\n  }\n\n  aclSetUser(username: string, ...rules: string[]) {\n    return this.execStatusReply(\"ACL\", \"SETUSER\", username, ...rules);\n  }\n\n  aclUsers() {\n    return this.execArrayReply<BulkString>(\"ACL\", \"USERS\");\n  }\n\n  aclWhoami() {\n    return this.execBulkReply<BulkString>(\"ACL\", \"WHOAMI\");\n  }\n\n  append(key: string, value: RedisValue) {\n    return this.execIntegerReply(\"APPEND\", key, value);\n  }\n\n  auth(param1: RedisValue, param2?: RedisValue) {\n    if (param2 !== undefined) {\n      return this.execStatusReply(\"AUTH\", param1, param2);\n    }\n    return this.execStatusReply(\"AUTH\", param1);\n  }\n\n  bgrewriteaof() {\n    return this.execStatusReply(\"BGREWRITEAOF\");\n  }\n\n  bgsave() {\n    return this.execStatusReply(\"BGSAVE\");\n  }\n\n  bitcount(key: string, start?: number, end?: number) {\n    if (start !== undefined && end !== undefined) {\n      return this.execIntegerReply(\"BITCOUNT\", key, start, end);\n    }\n    return this.execIntegerReply(\"BITCOUNT\", key);\n  }\n\n  bitfield(\n    key: string,\n    opts?: BitfieldOpts | BitfieldWithOverflowOpts,\n  ) {\n    const args: (number | string)[] = [key];\n    if (opts?.get) {\n      const { type, offset } = opts.get;\n      args.push(\"GET\", type, offset);\n    }\n    if (opts?.set) {\n      const { type, offset, value } = opts.set;\n      args.push(\"SET\", type, offset, value);\n    }\n    if (opts?.incrby) {\n      const { type, offset, increment } = opts.incrby;\n      args.push(\"INCRBY\", type, offset, increment);\n    }\n    if ((opts as BitfieldWithOverflowOpts)?.overflow) {\n      args.push(\"OVERFLOW\", (opts as BitfieldWithOverflowOpts).overflow);\n    }\n    return this.execArrayReply<Integer>(\"BITFIELD\", ...args);\n  }\n\n  bitop(operation: string, destkey: string, ...keys: string[]) {\n    return this.execIntegerReply(\"BITOP\", operation, destkey, ...keys);\n  }\n\n  bitpos(key: string, bit: number, start?: number, end?: number) {\n    if (start !== undefined && end !== undefined) {\n      return this.execIntegerReply(\"BITPOS\", key, bit, start, end);\n    }\n    if (start !== undefined) {\n      return this.execIntegerReply(\"BITPOS\", key, bit, start);\n    }\n    return this.execIntegerReply(\"BITPOS\", key, bit);\n  }\n\n  blpop(timeout: number, ...keys: string[]) {\n    return this.execArrayReply(\"BLPOP\", ...keys, timeout) as Promise<\n      [BulkString, BulkString] | BulkNil\n    >;\n  }\n\n  brpop(timeout: number, ...keys: string[]) {\n    return this.execArrayReply(\"BRPOP\", ...keys, timeout) as Promise<\n      [BulkString, BulkString] | BulkNil\n    >;\n  }\n\n  brpoplpush(source: string, destination: string, timeout: number) {\n    return this.execBulkReply(\"BRPOPLPUSH\", source, destination, timeout);\n  }\n\n  bzpopmin(timeout: number, ...keys: string[]) {\n    return this.execArrayReply(\"BZPOPMIN\", ...keys, timeout) as Promise<\n      [BulkString, BulkString, BulkString] | BulkNil\n    >;\n  }\n\n  bzpopmax(timeout: number, ...keys: string[]) {\n    return this.execArrayReply(\"BZPOPMAX\", ...keys, timeout) as Promise<\n      [BulkString, BulkString, BulkString] | BulkNil\n    >;\n  }\n\n  clientCaching(mode: ClientCachingMode) {\n    return this.execStatusReply(\"CLIENT\", \"CACHING\", mode);\n  }\n\n  clientGetName() {\n    return this.execBulkReply(\"CLIENT\", \"GETNAME\");\n  }\n\n  clientGetRedir() {\n    return this.execIntegerReply(\"CLIENT\", \"GETREDIR\");\n  }\n\n  clientID() {\n    return this.execIntegerReply(\"CLIENT\", \"ID\");\n  }\n\n  clientInfo() {\n    return this.execBulkReply(\"CLIENT\", \"INFO\");\n  }\n\n  clientKill(opts: ClientKillOpts) {\n    const args: (string | number)[] = [];\n    if (opts.addr) {\n      args.push(\"ADDR\", opts.addr);\n    }\n    if (opts.laddr) {\n      args.push(\"LADDR\", opts.laddr);\n    }\n    if (opts.id) {\n      args.push(\"ID\", opts.id);\n    }\n    if (opts.type) {\n      args.push(\"TYPE\", opts.type);\n    }\n    if (opts.user) {\n      args.push(\"USER\", opts.user);\n    }\n    if (opts.skipme) {\n      args.push(\"SKIPME\", opts.skipme);\n    }\n    return this.execIntegerReply(\"CLIENT\", \"KILL\", ...args);\n  }\n\n  clientList(opts?: ClientListOpts) {\n    if (opts && opts.type && opts.ids) {\n      throw new Error(\"only one of `type` or `ids` can be specified\");\n    }\n    if (opts && opts.type) {\n      return this.execBulkReply(\"CLIENT\", \"LIST\", \"TYPE\", opts.type);\n    }\n    if (opts && opts.ids) {\n      return this.execBulkReply(\"CLIENT\", \"LIST\", \"ID\", ...opts.ids);\n    }\n    return this.execBulkReply(\"CLIENT\", \"LIST\");\n  }\n\n  clientPause(timeout: number, mode?: ClientPauseMode) {\n    if (mode) {\n      return this.execStatusReply(\"CLIENT\", \"PAUSE\", timeout, mode);\n    }\n    return this.execStatusReply(\"CLIENT\", \"PAUSE\", timeout);\n  }\n\n  clientSetName(connectionName: string) {\n    return this.execStatusReply(\"CLIENT\", \"SETNAME\", connectionName);\n  }\n\n  clientTracking(opts: ClientTrackingOpts) {\n    const args: (number | string)[] = [opts.mode];\n    if (opts.redirect) {\n      args.push(\"REDIRECT\", opts.redirect);\n    }\n    if (opts.prefixes) {\n      opts.prefixes.forEach((prefix) => {\n        args.push(\"PREFIX\");\n        args.push(prefix);\n      });\n    }\n    if (opts.bcast) {\n      args.push(\"BCAST\");\n    }\n    if (opts.optIn) {\n      args.push(\"OPTIN\");\n    }\n    if (opts.optOut) {\n      args.push(\"OPTOUT\");\n    }\n    if (opts.noLoop) {\n      args.push(\"NOLOOP\");\n    }\n    return this.execStatusReply(\"CLIENT\", \"TRACKING\", ...args);\n  }\n\n  clientTrackingInfo() {\n    return this.execArrayReply(\"CLIENT\", \"TRACKINGINFO\");\n  }\n\n  clientUnblock(\n    id: number,\n    behaviour?: ClientUnblockingBehaviour,\n  ): Promise<Integer> {\n    if (behaviour) {\n      return this.execIntegerReply(\"CLIENT\", \"UNBLOCK\", id, behaviour);\n    }\n    return this.execIntegerReply(\"CLIENT\", \"UNBLOCK\", id);\n  }\n\n  clientUnpause(): Promise<SimpleString> {\n    return this.execStatusReply(\"CLIENT\", \"UNPAUSE\");\n  }\n\n  asking() {\n    return this.execStatusReply(\"ASKING\");\n  }\n\n  clusterAddSlots(...slots: number[]) {\n    return this.execStatusReply(\"CLUSTER\", \"ADDSLOTS\", ...slots);\n  }\n\n  clusterCountFailureReports(nodeId: string) {\n    return this.execIntegerReply(\"CLUSTER\", \"COUNT-FAILURE-REPORTS\", nodeId);\n  }\n\n  clusterCountKeysInSlot(slot: number) {\n    return this.execIntegerReply(\"CLUSTER\", \"COUNTKEYSINSLOT\", slot);\n  }\n\n  clusterDelSlots(...slots: number[]) {\n    return this.execStatusReply(\"CLUSTER\", \"DELSLOTS\", ...slots);\n  }\n\n  clusterFailover(mode?: ClusterFailoverMode) {\n    if (mode) {\n      return this.execStatusReply(\"CLUSTER\", \"FAILOVER\", mode);\n    }\n    return this.execStatusReply(\"CLUSTER\", \"FAILOVER\");\n  }\n\n  clusterFlushSlots() {\n    return this.execStatusReply(\"CLUSTER\", \"FLUSHSLOTS\");\n  }\n\n  clusterForget(nodeId: string) {\n    return this.execStatusReply(\"CLUSTER\", \"FORGET\", nodeId);\n  }\n\n  clusterGetKeysInSlot(slot: number, count: number) {\n    return this.execArrayReply<BulkString>(\n      \"CLUSTER\",\n      \"GETKEYSINSLOT\",\n      slot,\n      count,\n    );\n  }\n\n  clusterInfo() {\n    return this.execStatusReply(\"CLUSTER\", \"INFO\");\n  }\n\n  clusterKeySlot(key: string) {\n    return this.execIntegerReply(\"CLUSTER\", \"KEYSLOT\", key);\n  }\n\n  clusterMeet(ip: string, port: number) {\n    return this.execStatusReply(\"CLUSTER\", \"MEET\", ip, port);\n  }\n\n  clusterMyID() {\n    return this.execStatusReply(\"CLUSTER\", \"MYID\");\n  }\n\n  clusterNodes() {\n    return this.execBulkReply<BulkString>(\"CLUSTER\", \"NODES\");\n  }\n\n  clusterReplicas(nodeId: string) {\n    return this.execArrayReply<BulkString>(\"CLUSTER\", \"REPLICAS\", nodeId);\n  }\n\n  clusterReplicate(nodeId: string) {\n    return this.execStatusReply(\"CLUSTER\", \"REPLICATE\", nodeId);\n  }\n\n  clusterReset(mode?: ClusterResetMode) {\n    if (mode) {\n      return this.execStatusReply(\"CLUSTER\", \"RESET\", mode);\n    }\n    return this.execStatusReply(\"CLUSTER\", \"RESET\");\n  }\n\n  clusterSaveConfig() {\n    return this.execStatusReply(\"CLUSTER\", \"SAVECONFIG\");\n  }\n\n  clusterSetSlot(\n    slot: number,\n    subcommand: ClusterSetSlotSubcommand,\n    nodeId?: string,\n  ) {\n    if (nodeId !== undefined) {\n      return this.execStatusReply(\n        \"CLUSTER\",\n        \"SETSLOT\",\n        slot,\n        subcommand,\n        nodeId,\n      );\n    }\n    return this.execStatusReply(\"CLUSTER\", \"SETSLOT\", slot, subcommand);\n  }\n\n  clusterSlaves(nodeId: string) {\n    return this.execArrayReply<BulkString>(\"CLUSTER\", \"SLAVES\", nodeId);\n  }\n\n  clusterSlots() {\n    return this.execArrayReply(\"CLUSTER\", \"SLOTS\");\n  }\n\n  command() {\n    return this.execArrayReply(\"COMMAND\") as Promise<\n      [BulkString, Integer, BulkString[], Integer, Integer, Integer][]\n    >;\n  }\n\n  commandCount() {\n    return this.execIntegerReply(\"COMMAND\", \"COUNT\");\n  }\n\n  commandGetKeys() {\n    return this.execArrayReply<BulkString>(\"COMMAND\", \"GETKEYS\");\n  }\n\n  commandInfo(...commandNames: string[]) {\n    return this.execArrayReply(\"COMMAND\", \"INFO\", ...commandNames) as Promise<\n      (\n        | [BulkString, Integer, BulkString[], Integer, Integer, Integer]\n        | BulkNil\n      )[]\n    >;\n  }\n\n  configGet(parameter: string) {\n    return this.execArrayReply<BulkString>(\"CONFIG\", \"GET\", parameter);\n  }\n\n  configResetStat() {\n    return this.execStatusReply(\"CONFIG\", \"RESETSTAT\");\n  }\n\n  configRewrite() {\n    return this.execStatusReply(\"CONFIG\", \"REWRITE\");\n  }\n\n  configSet(parameter: string, value: string | number) {\n    return this.execStatusReply(\"CONFIG\", \"SET\", parameter, value);\n  }\n\n  dbsize() {\n    return this.execIntegerReply(\"DBSIZE\");\n  }\n\n  debugObject(key: string) {\n    return this.execStatusReply(\"DEBUG\", \"OBJECT\", key);\n  }\n\n  debugSegfault() {\n    return this.execStatusReply(\"DEBUG\", \"SEGFAULT\");\n  }\n\n  decr(key: string) {\n    return this.execIntegerReply(\"DECR\", key);\n  }\n\n  decrby(key: string, decrement: number) {\n    return this.execIntegerReply(\"DECRBY\", key, decrement);\n  }\n\n  del(...keys: string[]) {\n    return this.execIntegerReply(\"DEL\", ...keys);\n  }\n\n  discard() {\n    return this.execStatusReply(\"DISCARD\");\n  }\n\n  dump(key: string) {\n    return this.execBinaryReply(\"DUMP\", key);\n  }\n\n  echo(message: RedisValue) {\n    return this.execBulkReply<BulkString>(\"ECHO\", message);\n  }\n\n  eval(script: string, keys: string[], args: string[]) {\n    return this.execReply(\n      \"EVAL\",\n      script,\n      keys.length,\n      ...keys,\n      ...args,\n    );\n  }\n\n  evalsha(sha1: string, keys: string[], args: string[]) {\n    return this.execReply(\n      \"EVALSHA\",\n      sha1,\n      keys.length,\n      ...keys,\n      ...args,\n    );\n  }\n\n  exec() {\n    return this.execArrayReply(\"EXEC\");\n  }\n\n  exists(...keys: string[]) {\n    return this.execIntegerReply(\"EXISTS\", ...keys);\n  }\n\n  expire(key: string, seconds: number) {\n    return this.execIntegerReply(\"EXPIRE\", key, seconds);\n  }\n\n  expireat(key: string, timestamp: string) {\n    return this.execIntegerReply(\"EXPIREAT\", key, timestamp);\n  }\n\n  flushall(async?: boolean) {\n    if (async) {\n      return this.execStatusReply(\"FLUSHALL\", \"ASYNC\");\n    }\n    return this.execStatusReply(\"FLUSHALL\");\n  }\n\n  flushdb(async?: boolean) {\n    if (async) {\n      return this.execStatusReply(\"FLUSHDB\", \"ASYNC\");\n    }\n    return this.execStatusReply(\"FLUSHDB\");\n  }\n\n  // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any`\n  geoadd(key: string, ...params: any[]) {\n    const args: (string | number)[] = [key];\n    if (Array.isArray(params[0])) {\n      args.push(...params.flatMap((e) => e));\n    } else if (typeof params[0] === \"object\") {\n      for (const [member, lnglat] of Object.entries(params[0])) {\n        args.push(...(lnglat as [number, number]), member);\n      }\n    } else {\n      args.push(...params);\n    }\n    return this.execIntegerReply(\"GEOADD\", ...args);\n  }\n\n  geohash(key: string, ...members: string[]) {\n    return this.execArrayReply<Bulk>(\"GEOHASH\", key, ...members);\n  }\n\n  geopos(key: string, ...members: string[]) {\n    return this.execArrayReply(\"GEOPOS\", key, ...members) as Promise<\n      ([BulkString, BulkString] | BulkNil | [])[]\n    >;\n  }\n\n  geodist(\n    key: string,\n    member1: string,\n    member2: string,\n    unit?: GeoUnit,\n  ) {\n    if (unit) {\n      return this.execBulkReply(\"GEODIST\", key, member1, member2, unit);\n    }\n    return this.execBulkReply(\"GEODIST\", key, member1, member2);\n  }\n\n  georadius(\n    key: string,\n    longitude: number,\n    latitude: number,\n    radius: number,\n    unit: \"m\" | \"km\" | \"ft\" | \"mi\",\n    opts?: GeoRadiusOpts,\n  ) {\n    const args = this.pushGeoRadiusOpts(\n      [key, longitude, latitude, radius, unit],\n      opts,\n    );\n    return this.execArrayReply(\"GEORADIUS\", ...args);\n  }\n\n  georadiusbymember(\n    key: string,\n    member: string,\n    radius: number,\n    unit: GeoUnit,\n    opts?: GeoRadiusOpts,\n  ) {\n    const args = this.pushGeoRadiusOpts([key, member, radius, unit], opts);\n    return this.execArrayReply(\"GEORADIUSBYMEMBER\", ...args);\n  }\n\n  private pushGeoRadiusOpts(\n    args: (string | number)[],\n    opts?: GeoRadiusOpts,\n  ) {\n    if (opts?.withCoord) {\n      args.push(\"WITHCOORD\");\n    }\n    if (opts?.withDist) {\n      args.push(\"WITHDIST\");\n    }\n    if (opts?.withHash) {\n      args.push(\"WITHHASH\");\n    }\n    if (opts?.count !== undefined) {\n      args.push(opts.count);\n    }\n    if (opts?.sort) {\n      args.push(opts.sort);\n    }\n    if (opts?.store !== undefined) {\n      args.push(opts.store);\n    }\n    if (opts?.storeDist !== undefined) {\n      args.push(opts.storeDist);\n    }\n    return args;\n  }\n\n  get(key: string) {\n    return this.execBulkReply(\"GET\", key);\n  }\n\n  getbit(key: string, offset: number) {\n    return this.execIntegerReply(\"GETBIT\", key, offset);\n  }\n\n  getrange(key: string, start: number, end: number) {\n    return this.execBulkReply<BulkString>(\"GETRANGE\", key, start, end);\n  }\n\n  getset(key: string, value: RedisValue) {\n    return this.execBulkReply(\"GETSET\", key, value);\n  }\n\n  hdel(key: string, ...fields: string[]) {\n    return this.execIntegerReply(\"HDEL\", key, ...fields);\n  }\n\n  hexists(key: string, field: string) {\n    return this.execIntegerReply(\"HEXISTS\", key, field);\n  }\n\n  hget(key: string, field: string) {\n    return this.execBulkReply(\"HGET\", key, field);\n  }\n\n  hgetall(key: string) {\n    return this.execArrayReply<BulkString>(\"HGETALL\", key);\n  }\n\n  hincrby(key: string, field: string, increment: number) {\n    return this.execIntegerReply(\"HINCRBY\", key, field, increment);\n  }\n\n  hincrbyfloat(key: string, field: string, increment: number) {\n    return this.execBulkReply<BulkString>(\n      \"HINCRBYFLOAT\",\n      key,\n      field,\n      increment,\n    );\n  }\n\n  hkeys(key: string) {\n    return this.execArrayReply<BulkString>(\"HKEYS\", key);\n  }\n\n  hlen(key: string) {\n    return this.execIntegerReply(\"HLEN\", key);\n  }\n\n  hmget(key: string, ...fields: string[]) {\n    return this.execArrayReply<Bulk>(\"HMGET\", key, ...fields);\n  }\n\n  // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any`\n  hmset(key: string, ...params: any[]) {\n    const args = [key] as RedisValue[];\n    if (Array.isArray(params[0])) {\n      args.push(...params.flatMap((e) => e));\n    } else if (typeof params[0] === \"object\") {\n      for (const [field, value] of Object.entries(params[0])) {\n        args.push(field, value as RedisValue);\n      }\n    } else {\n      args.push(...params);\n    }\n    return this.execStatusReply(\"HMSET\", ...args);\n  }\n\n  // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any`\n  hset(key: string, ...params: any[]) {\n    const args = [key] as RedisValue[];\n    if (Array.isArray(params[0])) {\n      args.push(...params.flatMap((e) => e));\n    } else if (typeof params[0] === \"object\") {\n      for (const [field, value] of Object.entries(params[0])) {\n        args.push(field, value as RedisValue);\n      }\n    } else {\n      args.push(...params);\n    }\n    return this.execIntegerReply(\"HSET\", ...args);\n  }\n\n  hsetnx(key: string, field: string, value: RedisValue) {\n    return this.execIntegerReply(\"HSETNX\", key, field, value);\n  }\n\n  hstrlen(key: string, field: string) {\n    return this.execIntegerReply(\"HSTRLEN\", key, field);\n  }\n\n  hvals(key: string) {\n    return this.execArrayReply<BulkString>(\"HVALS\", key);\n  }\n\n  incr(key: string) {\n    return this.execIntegerReply(\"INCR\", key);\n  }\n\n  incrby(key: string, increment: number) {\n    return this.execIntegerReply(\"INCRBY\", key, increment);\n  }\n\n  incrbyfloat(key: string, increment: number) {\n    return this.execBulkReply<BulkString>(\"INCRBYFLOAT\", key, increment);\n  }\n\n  info(section?: string) {\n    if (section !== undefined) {\n      return this.execStatusReply(\"INFO\", section);\n    }\n    return this.execStatusReply(\"INFO\");\n  }\n\n  keys(pattern: string) {\n    return this.execArrayReply<BulkString>(\"KEYS\", pattern);\n  }\n\n  lastsave() {\n    return this.execIntegerReply(\"LASTSAVE\");\n  }\n\n  lindex(key: string, index: number) {\n    return this.execBulkReply(\"LINDEX\", key, index);\n  }\n\n  linsert(key: string, loc: LInsertLocation, pivot: string, value: RedisValue) {\n    return this.execIntegerReply(\"LINSERT\", key, loc, pivot, value);\n  }\n\n  llen(key: string) {\n    return this.execIntegerReply(\"LLEN\", key);\n  }\n\n  lpop(key: string): Promise<Bulk>;\n  lpop(key: string, count: number): Promise<Array<BulkString>>;\n  lpop(key: string, count?: number): Promise<Bulk | Array<BulkString>> {\n    if (count == null) {\n      return this.execBulkReply(\"LPOP\", key);\n    } else {\n      return this.execArrayReply(\"LPOP\", key, count);\n    }\n  }\n\n  lpos(\n    key: string,\n    element: RedisValue,\n    opts?: LPosOpts,\n  ): Promise<Integer | BulkNil>;\n\n  lpos(\n    key: string,\n    element: RedisValue,\n    opts: LPosWithCountOpts,\n  ): Promise<Integer[]>;\n\n  lpos(\n    key: string,\n    element: RedisValue,\n    opts?: LPosOpts | LPosWithCountOpts,\n  ): Promise<Integer | BulkNil | Integer[]> {\n    const args = [element];\n    if (opts?.rank != null) {\n      args.push(\"RANK\", String(opts.rank));\n    }\n\n    if (opts?.count != null) {\n      args.push(\"COUNT\", String(opts.count));\n    }\n\n    if (opts?.maxlen != null) {\n      args.push(\"MAXLEN\", String(opts.maxlen));\n    }\n\n    return opts?.count == null\n      ? this.execIntegerReply(\"LPOS\", key, ...args)\n      : this.execArrayReply<Integer>(\"LPOS\", key, ...args);\n  }\n\n  lpush(key: string, ...elements: RedisValue[]) {\n    return this.execIntegerReply(\"LPUSH\", key, ...elements);\n  }\n\n  lpushx(key: string, ...elements: RedisValue[]) {\n    return this.execIntegerReply(\"LPUSHX\", key, ...elements);\n  }\n\n  lrange(key: string, start: number, stop: number) {\n    return this.execArrayReply<BulkString>(\"LRANGE\", key, start, stop);\n  }\n\n  lrem(key: string, count: number, element: string | number) {\n    return this.execIntegerReply(\"LREM\", key, count, element);\n  }\n\n  lset(key: string, index: number, element: string | number) {\n    return this.execStatusReply(\"LSET\", key, index, element);\n  }\n\n  ltrim(key: string, start: number, stop: number) {\n    return this.execStatusReply(\"LTRIM\", key, start, stop);\n  }\n\n  memoryDoctor() {\n    return this.execBulkReply<BulkString>(\"MEMORY\", \"DOCTOR\");\n  }\n\n  memoryHelp() {\n    return this.execArrayReply<BulkString>(\"MEMORY\", \"HELP\");\n  }\n\n  memoryMallocStats() {\n    return this.execBulkReply<BulkString>(\"MEMORY\", \"MALLOC\", \"STATS\");\n  }\n\n  memoryPurge() {\n    return this.execStatusReply(\"MEMORY\", \"PURGE\");\n  }\n\n  memoryStats() {\n    return this.execArrayReply(\"MEMORY\", \"STATS\");\n  }\n\n  memoryUsage(key: string, opts?: MemoryUsageOpts) {\n    const args: (number | string)[] = [key];\n    if (opts?.samples !== undefined) {\n      args.push(\"SAMPLES\", opts.samples);\n    }\n    return this.execIntegerReply(\"MEMORY\", \"USAGE\", ...args);\n  }\n\n  mget(...keys: string[]) {\n    return this.execArrayReply<Bulk>(\"MGET\", ...keys);\n  }\n\n  migrate(\n    host: string,\n    port: number,\n    key: string,\n    destinationDB: string,\n    timeout: number,\n    opts?: MigrateOpts,\n  ) {\n    const args = [host, port, key, destinationDB, timeout];\n    if (opts?.copy) {\n      args.push(\"COPY\");\n    }\n    if (opts?.replace) {\n      args.push(\"REPLACE\");\n    }\n    if (opts?.auth !== undefined) {\n      args.push(\"AUTH\", opts.auth);\n    }\n    if (opts?.keys) {\n      args.push(\"KEYS\", ...opts.keys);\n    }\n    return this.execStatusReply(\"MIGRATE\", ...args);\n  }\n\n  moduleList() {\n    return this.execArrayReply<BulkString>(\"MODULE\", \"LIST\");\n  }\n\n  moduleLoad(path: string, ...args: string[]) {\n    return this.execStatusReply(\"MODULE\", \"LOAD\", path, ...args);\n  }\n\n  moduleUnload(name: string) {\n    return this.execStatusReply(\"MODULE\", \"UNLOAD\", name);\n  }\n\n  monitor() {\n    throw new Error(\"not supported yet\");\n  }\n\n  move(key: string, db: string) {\n    return this.execIntegerReply(\"MOVE\", key, db);\n  }\n\n  // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any`\n  mset(...params: any[]) {\n    const args: RedisValue[] = [];\n    if (Array.isArray(params[0])) {\n      args.push(...params.flatMap((e) => e));\n    } else if (typeof params[0] === \"object\") {\n      for (const [key, value] of Object.entries(params[0])) {\n        args.push(key, value as RedisValue);\n      }\n    } else {\n      args.push(...params);\n    }\n    return this.execStatusReply(\"MSET\", ...args);\n  }\n\n  // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any`\n  msetnx(...params: any[]) {\n    const args: RedisValue[] = [];\n    if (Array.isArray(params[0])) {\n      args.push(...params.flatMap((e) => e));\n    } else if (typeof params[0] === \"object\") {\n      for (const [key, value] of Object.entries(params[0])) {\n        args.push(key, value as RedisValue);\n      }\n    } else {\n      args.push(...params);\n    }\n    return this.execIntegerReply(\"MSETNX\", ...args);\n  }\n\n  multi() {\n    return this.execStatusReply(\"MULTI\");\n  }\n\n  objectEncoding(key: string) {\n    return this.execBulkReply(\"OBJECT\", \"ENCODING\", key);\n  }\n\n  objectFreq(key: string) {\n    return this.execIntegerOrNilReply(\"OBJECT\", \"FREQ\", key);\n  }\n\n  objectHelp() {\n    return this.execArrayReply<BulkString>(\"OBJECT\", \"HELP\");\n  }\n\n  objectIdletime(key: string) {\n    return this.execIntegerOrNilReply(\"OBJECT\", \"IDLETIME\", key);\n  }\n\n  objectRefCount(key: string) {\n    return this.execIntegerOrNilReply(\"OBJECT\", \"REFCOUNT\", key);\n  }\n\n  persist(key: string) {\n    return this.execIntegerReply(\"PERSIST\", key);\n  }\n\n  pexpire(key: string, milliseconds: number) {\n    return this.execIntegerReply(\"PEXPIRE\", key, milliseconds);\n  }\n\n  pexpireat(key: string, millisecondsTimestamp: number) {\n    return this.execIntegerReply(\"PEXPIREAT\", key, millisecondsTimestamp);\n  }\n\n  pfadd(key: string, ...elements: string[]) {\n    return this.execIntegerReply(\"PFADD\", key, ...elements);\n  }\n\n  pfcount(...keys: string[]) {\n    return this.execIntegerReply(\"PFCOUNT\", ...keys);\n  }\n\n  pfmerge(destkey: string, ...sourcekeys: string[]) {\n    return this.execStatusReply(\"PFMERGE\", destkey, ...sourcekeys);\n  }\n\n  ping(message?: RedisValue) {\n    if (message) {\n      return this.execBulkReply<BulkString>(\"PING\", message);\n    }\n    return this.execStatusReply(\"PING\");\n  }\n\n  psetex(key: string, milliseconds: number, value: RedisValue) {\n    return this.execStatusReply(\"PSETEX\", key, milliseconds, value);\n  }\n\n  publish(channel: string, message: string) {\n    return this.execIntegerReply(\"PUBLISH\", channel, message);\n  }\n\n  // deno-lint-ignore no-explicit-any -- This is a private property.\n  #subscription?: RedisSubscription<any>;\n  async subscribe<TMessage extends string | string[] = string>(\n    ...channels: string[]\n  ) {\n    if (this.#subscription) {\n      await this.#subscription.subscribe(...channels);\n      return this.#subscription;\n    }\n    const subscription = await this.client.subscribe<TMessage>(\n      \"SUBSCRIBE\",\n      ...channels,\n    );\n    this.#subscription = subscription;\n    return subscription;\n  }\n\n  async psubscribe<TMessage extends string | string[] = string>(\n    ...patterns: string[]\n  ) {\n    if (this.#subscription) {\n      await this.#subscription.psubscribe(...patterns);\n      return this.#subscription;\n    }\n    const subscription = await this.client.subscribe<TMessage>(\n      \"PSUBSCRIBE\",\n      ...patterns,\n    );\n    this.#subscription = subscription;\n    return subscription;\n  }\n\n  pubsubChannels(pattern?: string) {\n    if (pattern !== undefined) {\n      return this.execArrayReply<BulkString>(\"PUBSUB\", \"CHANNELS\", pattern);\n    }\n    return this.execArrayReply<BulkString>(\"PUBSUB\", \"CHANNELS\");\n  }\n\n  pubsubNumpat() {\n    return this.execIntegerReply(\"PUBSUB\", \"NUMPAT\");\n  }\n\n  pubsubNumsub(...channels: string[]) {\n    return this.execArrayReply<BulkString | Integer>(\n      \"PUBSUB\",\n      \"NUMSUB\",\n      ...channels,\n    );\n  }\n\n  pttl(key: string) {\n    return this.execIntegerReply(\"PTTL\", key);\n  }\n\n  quit() {\n    return this.execStatusReply(\"QUIT\").finally(() => this.close());\n  }\n\n  randomkey() {\n    return this.execBulkReply(\"RANDOMKEY\");\n  }\n\n  readonly() {\n    return this.execStatusReply(\"READONLY\");\n  }\n\n  readwrite() {\n    return this.execStatusReply(\"READWRITE\");\n  }\n\n  rename(key: string, newkey: string) {\n    return this.execStatusReply(\"RENAME\", key, newkey);\n  }\n\n  renamenx(key: string, newkey: string) {\n    return this.execIntegerReply(\"RENAMENX\", key, newkey);\n  }\n\n  restore(\n    key: string,\n    ttl: number,\n    serializedValue: Binary,\n    opts?: RestoreOpts,\n  ) {\n    const args = [key, ttl, serializedValue];\n    if (opts?.replace) {\n      args.push(\"REPLACE\");\n    }\n    if (opts?.absttl) {\n      args.push(\"ABSTTL\");\n    }\n    if (opts?.idletime !== undefined) {\n      args.push(\"IDLETIME\", opts.idletime);\n    }\n    if (opts?.freq !== undefined) {\n      args.push(\"FREQ\", opts.freq);\n    }\n    return this.execStatusReply(\"RESTORE\", ...args);\n  }\n\n  role() {\n    return this.execArrayReply(\"ROLE\") as Promise<\n      | [\"master\", Integer, BulkString[][]]\n      | [\"slave\", BulkString, Integer, BulkString, Integer]\n      | [\"sentinel\", BulkString[]]\n    >;\n  }\n\n  rpop(key: string) {\n    return this.execBulkReply(\"RPOP\", key);\n  }\n\n  rpoplpush(source: string, destination: string) {\n    return this.execBulkReply(\"RPOPLPUSH\", source, destination);\n  }\n\n  rpush(key: string, ...elements: RedisValue[]) {\n    return this.execIntegerReply(\"RPUSH\", key, ...elements);\n  }\n\n  rpushx(key: string, ...elements: RedisValue[]) {\n    return this.execIntegerReply(\"RPUSHX\", key, ...elements);\n  }\n\n  sadd(key: string, ...members: string[]) {\n    return this.execIntegerReply(\"SADD\", key, ...members);\n  }\n\n  save() {\n    return this.execStatusReply(\"SAVE\");\n  }\n\n  scard(key: string) {\n    return this.execIntegerReply(\"SCARD\", key);\n  }\n\n  scriptDebug(mode: ScriptDebugMode) {\n    return this.execStatusReply(\"SCRIPT\", \"DEBUG\", mode);\n  }\n\n  scriptExists(...sha1s: string[]) {\n    return this.execArrayReply<Integer>(\"SCRIPT\", \"EXISTS\", ...sha1s);\n  }\n\n  scriptFlush() {\n    return this.execStatusReply(\"SCRIPT\", \"FLUSH\");\n  }\n\n  scriptKill() {\n    return this.execStatusReply(\"SCRIPT\", \"KILL\");\n  }\n\n  scriptLoad(script: string) {\n    return this.execStatusReply(\"SCRIPT\", \"LOAD\", script);\n  }\n\n  sdiff(...keys: string[]) {\n    return this.execArrayReply<BulkString>(\"SDIFF\", ...keys);\n  }\n\n  sdiffstore(destination: string, ...keys: string[]) {\n    return this.execIntegerReply(\"SDIFFSTORE\", destination, ...keys);\n  }\n\n  select(index: number) {\n    return this.execStatusReply(\"SELECT\", index);\n  }\n\n  hello(opts?: HelloOpts): Promise<ConditionalArray> {\n    const args: Array<RedisValue> = [];\n    if (opts?.protover != null) {\n      args.push(opts.protover);\n    }\n    if (opts?.auth != null) {\n      args.push(\"AUTH\", opts.auth.username, opts.auth.password);\n    }\n    if (opts?.clientName != null) {\n      args.push(\"SETNAME\", opts.clientName);\n    }\n    return this.execArrayReply(\"HELLO\", ...args);\n  }\n\n  set<TSetOpts extends SetOpts | SetWithModeOpts = SetOpts>(\n    key: string,\n    value: RedisValue,\n    opts?: TSetOpts,\n  ): Promise<SetReply<TSetOpts>> {\n    const args: RedisValue[] = [key, value];\n    if (opts?.ex != null) {\n      args.push(\"EX\", opts.ex);\n    } else if (opts?.px != null) {\n      args.push(\"PX\", opts.px);\n    } else if (opts?.exat != null) {\n      args.push(\"EXAT\", opts.exat);\n    } else if (opts?.pxat != null) {\n      args.push(\"PXAT\", opts.pxat);\n    }\n    // TODO: Isn't `KEEPTTL` option exclusive with `EX`, `PX`, etc.?\n    if (opts?.keepttl) {\n      args.push(\"KEEPTTL\");\n    }\n\n    let isAbleToReturnNil = false;\n    if (opts?.nx) {\n      args.push(\"NX\");\n      isAbleToReturnNil = true;\n    } else if (opts?.xx) {\n      args.push(\"XX\");\n      isAbleToReturnNil = true;\n    } else if ((opts as SetWithModeOpts)?.mode) {\n      args.push((opts as SetWithModeOpts).mode);\n      isAbleToReturnNil = true;\n    }\n    if (opts?.get) {\n      args.push(\"GET\");\n    }\n    const promise = isAbleToReturnNil\n      ? this.execStatusOrNilReply(\"SET\", ...args)\n      : this.execStatusReply(\"SET\", ...args);\n    return promise as Promise<SetReply<TSetOpts>>;\n  }\n\n  setbit(key: string, offset: number, value: RedisValue) {\n    return this.execIntegerReply(\"SETBIT\", key, offset, value);\n  }\n\n  setex(key: string, seconds: number, value: RedisValue) {\n    return this.execStatusReply(\"SETEX\", key, seconds, value);\n  }\n\n  setnx(key: string, value: RedisValue) {\n    return this.execIntegerReply(\"SETNX\", key, value);\n  }\n\n  setrange(key: string, offset: number, value: RedisValue) {\n    return this.execIntegerReply(\"SETRANGE\", key, offset, value);\n  }\n\n  shutdown(mode?: ShutdownMode) {\n    if (mode) {\n      return this.execStatusReply(\"SHUTDOWN\", mode);\n    }\n    return this.execStatusReply(\"SHUTDOWN\");\n  }\n\n  sinter(...keys: string[]) {\n    return this.execArrayReply<BulkString>(\"SINTER\", ...keys);\n  }\n\n  sinterstore(destination: string, ...keys: string[]) {\n    return this.execIntegerReply(\"SINTERSTORE\", destination, ...keys);\n  }\n\n  sismember(key: string, member: string) {\n    return this.execIntegerReply(\"SISMEMBER\", key, member);\n  }\n\n  slaveof(host: string, port: number) {\n    return this.execStatusReply(\"SLAVEOF\", host, port);\n  }\n\n  slaveofNoOne() {\n    return this.execStatusReply(\"SLAVEOF\", \"NO ONE\");\n  }\n\n  replicaof(host: string, port: number) {\n    return this.execStatusReply(\"REPLICAOF\", host, port);\n  }\n\n  replicaofNoOne() {\n    return this.execStatusReply(\"REPLICAOF\", \"NO ONE\");\n  }\n\n  slowlog(subcommand: string, ...args: string[]) {\n    return this.execArrayReply(\"SLOWLOG\", subcommand, ...args);\n  }\n\n  smembers(key: string) {\n    return this.execArrayReply<BulkString>(\"SMEMBERS\", key);\n  }\n\n  smove(source: string, destination: string, member: string) {\n    return this.execIntegerReply(\"SMOVE\", source, destination, member);\n  }\n\n  sort(\n    key: string,\n    opts?: SortOpts,\n  ): Promise<BulkString[]>;\n  sort(\n    key: string,\n    opts?: SortWithDestinationOpts,\n  ): Promise<Integer>;\n  sort(\n    key: string,\n    opts?: SortOpts | SortWithDestinationOpts,\n  ) {\n    const args: (number | string)[] = [key];\n    if (opts?.by !== undefined) {\n      args.push(\"BY\", opts.by);\n    }\n    if (opts?.limit) {\n      args.push(\"LIMIT\", opts.limit.offset, opts.limit.count);\n    }\n    if (opts?.patterns) {\n      args.push(...opts.patterns.flatMap((pattern) => [\"GET\", pattern]));\n    }\n    if (opts?.order) {\n      args.push(opts.order);\n    }\n    if (opts?.alpha) {\n      args.push(\"ALPHA\");\n    }\n    if ((opts as SortWithDestinationOpts)?.destination !== undefined) {\n      args.push(\"STORE\", (opts as SortWithDestinationOpts).destination);\n      return this.execIntegerReply(\"SORT\", ...args);\n    }\n    return this.execArrayReply<BulkString>(\"SORT\", ...args);\n  }\n\n  spop(key: string): Promise<Bulk>;\n  spop(key: string, count: number): Promise<BulkString[]>;\n  spop(key: string, count?: number) {\n    if (count !== undefined) {\n      return this.execArrayReply<BulkString>(\"SPOP\", key, count);\n    }\n    return this.execBulkReply(\"SPOP\", key);\n  }\n\n  srandmember(key: string): Promise<Bulk>;\n  srandmember(key: string, count: number): Promise<BulkString[]>;\n  srandmember(key: string, count?: number) {\n    if (count !== undefined) {\n      return this.execArrayReply<BulkString>(\"SRANDMEMBER\", key, count);\n    }\n    return this.execBulkReply(\"SRANDMEMBER\", key);\n  }\n\n  srem(key: string, ...members: string[]) {\n    return this.execIntegerReply(\"SREM\", key, ...members);\n  }\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n  ): Promise<Bulk>;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { len: true },\n  ): Promise<Integer>;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { idx: true },\n  ): Promise<\n    [\n      string, //`\"matches\"`\n      Array<[[number, number], [number, number]]>,\n      string, // `\"len\"`\n      Integer,\n    ]\n  >;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: { idx: true; withmatchlen: true },\n  ): Promise<\n    [\n      string, // `\"matches\"`\n      Array<[[number, number], [number, number], number]>,\n      string, // `\"len\"`\n      Integer,\n    ]\n  >;\n\n  stralgo(\n    algorithm: StralgoAlgorithm,\n    target: StralgoTarget,\n    a: string,\n    b: string,\n    opts?: StralgoOpts,\n  ) {\n    const args: (number | string)[] = [];\n    if (opts?.idx) {\n      args.push(\"IDX\");\n    }\n    if (opts?.len) {\n      args.push(\"LEN\");\n    }\n    if (opts?.withmatchlen) {\n      args.push(\"WITHMATCHLEN\");\n    }\n    if (opts?.minmatchlen) {\n      args.push(\"MINMATCHLEN\");\n      args.push(opts.minmatchlen);\n    }\n    return this.execReply<Bulk | Integer | ConditionalArray>(\n      \"STRALGO\",\n      algorithm,\n      target,\n      a,\n      b,\n      ...args,\n    );\n  }\n\n  strlen(key: string) {\n    return this.execIntegerReply(\"STRLEN\", key);\n  }\n\n  sunion(...keys: string[]) {\n    return this.execArrayReply<BulkString>(\"SUNION\", ...keys);\n  }\n\n  sunionstore(destination: string, ...keys: string[]) {\n    return this.execIntegerReply(\"SUNIONSTORE\", destination, ...keys);\n  }\n\n  swapdb(index1: number, index2: number) {\n    return this.execStatusReply(\"SWAPDB\", index1, index2);\n  }\n\n  sync() {\n    throw new Error(\"not implemented\");\n  }\n\n  time() {\n    return this.execArrayReply(\"TIME\") as Promise<[BulkString, BulkString]>;\n  }\n\n  touch(...keys: string[]) {\n    return this.execIntegerReply(\"TOUCH\", ...keys);\n  }\n\n  ttl(key: string) {\n    return this.execIntegerReply(\"TTL\", key);\n  }\n\n  type(key: string) {\n    return this.execStatusReply(\"TYPE\", key);\n  }\n\n  unlink(...keys: string[]) {\n    return this.execIntegerReply(\"UNLINK\", ...keys);\n  }\n\n  unwatch() {\n    return this.execStatusReply(\"UNWATCH\");\n  }\n\n  wait(numreplicas: number, timeout: number) {\n    return this.execIntegerReply(\"WAIT\", numreplicas, timeout);\n  }\n\n  watch(...keys: string[]) {\n    return this.execStatusReply(\"WATCH\", ...keys);\n  }\n\n  xack(key: string, group: string, ...xids: XIdInput[]) {\n    return this.execIntegerReply(\n      \"XACK\",\n      key,\n      group,\n      ...xids.map((xid) => xidstr(xid)),\n    );\n  }\n\n  xadd(\n    key: string,\n    xid: XIdAdd,\n    fieldValues: XAddFieldValues,\n    maxlen: XMaxlen | undefined = undefined,\n  ) {\n    const args: RedisValue[] = [key];\n\n    if (maxlen) {\n      args.push(\"MAXLEN\");\n      if (maxlen.approx) {\n        args.push(\"~\");\n      }\n      args.push(maxlen.elements.toString());\n    }\n\n    args.push(xidstr(xid));\n\n    if (fieldValues instanceof Map) {\n      for (const [f, v] of fieldValues) {\n        args.push(f);\n        args.push(v);\n      }\n    } else {\n      for (const [f, v] of Object.entries(fieldValues)) {\n        args.push(f);\n        args.push(v);\n      }\n    }\n\n    return this.execBulkReply<BulkString>(\n      \"XADD\",\n      ...args,\n    ).then((rawId) => parseXId(rawId));\n  }\n\n  xclaim(key: string, opts: XClaimOpts, ...xids: XIdInput[]) {\n    const args = [];\n    if (opts.idle) {\n      args.push(\"IDLE\");\n      args.push(opts.idle);\n    }\n\n    if (opts.time) {\n      args.push(\"TIME\");\n      args.push(opts.time);\n    }\n\n    if (opts.retryCount) {\n      args.push(\"RETRYCOUNT\");\n      args.push(opts.retryCount);\n    }\n\n    if (opts.force) {\n      args.push(\"FORCE\");\n    }\n\n    if (opts.justXId) {\n      args.push(\"JUSTID\");\n    }\n\n    return this.execArrayReply<XReadIdData | BulkString>(\n      \"XCLAIM\",\n      key,\n      opts.group,\n      opts.consumer,\n      opts.minIdleTime,\n      ...xids.map((xid) => xidstr(xid)),\n      ...args,\n    ).then((raw) => {\n      if (opts.justXId) {\n        const xids = [];\n        for (const r of raw) {\n          if (typeof r === \"string\") {\n            xids.push(parseXId(r));\n          }\n        }\n        const payload: XClaimJustXId = { kind: \"justxid\", xids };\n        return payload;\n      }\n\n      const messages = [];\n      for (const r of raw) {\n        if (typeof r !== \"string\") {\n          messages.push(parseXMessage(r));\n        }\n      }\n      const payload: XClaimMessages = { kind: \"messages\", messages };\n      return payload;\n    });\n  }\n\n  xdel(key: string, ...xids: XIdInput[]) {\n    return this.execIntegerReply(\n      \"XDEL\",\n      key,\n      ...xids.map((rawId) => xidstr(rawId)),\n    );\n  }\n\n  xlen(key: string) {\n    return this.execIntegerReply(\"XLEN\", key);\n  }\n\n  xgroupCreate(\n    key: string,\n    groupName: string,\n    xid: XIdInput | \"$\",\n    mkstream?: boolean,\n  ) {\n    const args = [];\n    if (mkstream) {\n      args.push(\"MKSTREAM\");\n    }\n\n    return this.execStatusReply(\n      \"XGROUP\",\n      \"CREATE\",\n      key,\n      groupName,\n      xidstr(xid),\n      ...args,\n    );\n  }\n\n  xgroupDelConsumer(\n    key: string,\n    groupName: string,\n    consumerName: string,\n  ) {\n    return this.execIntegerReply(\n      \"XGROUP\",\n      \"DELCONSUMER\",\n      key,\n      groupName,\n      consumerName,\n    );\n  }\n\n  xgroupDestroy(key: string, groupName: string) {\n    return this.execIntegerReply(\"XGROUP\", \"DESTROY\", key, groupName);\n  }\n\n  xgroupHelp() {\n    return this.execBulkReply<BulkString>(\"XGROUP\", \"HELP\");\n  }\n\n  xgroupSetID(\n    key: string,\n    groupName: string,\n    xid: XId,\n  ) {\n    return this.execStatusReply(\n      \"XGROUP\",\n      \"SETID\",\n      key,\n      groupName,\n      xidstr(xid),\n    );\n  }\n\n  xinfoStream(key: string) {\n    return this.execArrayReply<Raw>(\"XINFO\", \"STREAM\", key).then(\n      (raw) => {\n        // Note that you should not rely on the fields\n        // exact position, nor on the number of fields,\n        // new fields may be added in the future.\n        const data: Map<string, Raw> = convertMap(raw);\n\n        const firstEntry = parseXMessage(\n          data.get(\"first-entry\") as XReadIdData,\n        );\n        const lastEntry = parseXMessage(\n          data.get(\"last-entry\") as XReadIdData,\n        );\n\n        return {\n          length: rawnum(data.get(\"length\") ?? null),\n          radixTreeKeys: rawnum(data.get(\"radix-tree-keys\") ?? null),\n          radixTreeNodes: rawnum(data.get(\"radix-tree-nodes\") ?? null),\n          groups: rawnum(data.get(\"groups\") ?? null),\n          lastGeneratedId: parseXId(\n            rawstr(data.get(\"last-generated-id\") ?? null),\n          ),\n          firstEntry,\n          lastEntry,\n        };\n      },\n    );\n  }\n\n  xinfoStreamFull(key: string, count?: number) {\n    const args = [];\n    if (count !== undefined) {\n      args.push(\"COUNT\");\n      args.push(count);\n    }\n    return this.execArrayReply<Raw>(\"XINFO\", \"STREAM\", key, \"FULL\", ...args)\n      .then(\n        (raw) => {\n          // Note that you should not rely on the fields\n          // exact position, nor on the number of fields,\n          // new fields may be added in the future.\n          if (raw == null) throw \"no data\";\n\n          const data: Map<string, Raw> = convertMap(raw);\n          if (data === undefined) throw \"no data converted\";\n\n          const entries = (data.get(\"entries\") as ConditionalArray).map((\n            raw: Raw,\n          ) => parseXMessage(raw as XReadIdData));\n          return {\n            length: rawnum(data.get(\"length\") ?? null),\n            radixTreeKeys: rawnum(data.get(\"radix-tree-keys\") ?? null),\n            radixTreeNodes: rawnum(data.get(\"radix-tree-nodes\") ?? null),\n            lastGeneratedId: parseXId(\n              rawstr(data.get(\"last-generated-id\") ?? null),\n            ),\n            entries,\n            groups: parseXGroupDetail(data.get(\"groups\") as ConditionalArray),\n          };\n        },\n      );\n  }\n\n  xinfoGroups(key: string) {\n    return this.execArrayReply<ConditionalArray>(\"XINFO\", \"GROUPS\", key).then(\n      (raws) =>\n        raws.map((raw) => {\n          const data = convertMap(raw);\n          return {\n            name: rawstr(data.get(\"name\") ?? null),\n            consumers: rawnum(data.get(\"consumers\") ?? null),\n            pending: rawnum(data.get(\"pending\") ?? null),\n            lastDeliveredId: parseXId(\n              rawstr(data.get(\"last-delivered-id\") ?? null),\n            ),\n          };\n        }),\n    );\n  }\n\n  xinfoConsumers(key: string, group: string) {\n    return this.execArrayReply<ConditionalArray>(\n      \"XINFO\",\n      \"CONSUMERS\",\n      key,\n      group,\n    ).then(\n      (raws) =>\n        raws.map((raw) => {\n          const data = convertMap(raw);\n          return {\n            name: rawstr(data.get(\"name\") ?? null),\n            pending: rawnum(data.get(\"pending\") ?? null),\n            idle: rawnum(data.get(\"idle\") ?? null),\n          };\n        }),\n    );\n  }\n\n  xpending(\n    key: string,\n    group: string,\n  ) {\n    return this.execArrayReply<Raw>(\"XPENDING\", key, group)\n      .then((raw) => {\n        if (\n          isNumber(raw[0]) && isString(raw[1]) &&\n          isString(raw[2]) && isCondArray(raw[3])\n        ) {\n          return {\n            count: raw[0],\n            startId: parseXId(raw[1]),\n            endId: parseXId(raw[2]),\n            consumers: parseXPendingConsumers(raw[3]),\n          };\n        } else {\n          throw \"parse err\";\n        }\n      });\n  }\n\n  xpendingCount(\n    key: string,\n    group: string,\n    startEndCount: StartEndCount,\n    consumer?: string,\n  ) {\n    const args = [];\n    args.push(xidstr(startEndCount.start));\n    args.push(xidstr(startEndCount.end));\n    args.push(startEndCount.count);\n\n    if (consumer) {\n      args.push(consumer);\n    }\n\n    return this.execArrayReply<Raw>(\"XPENDING\", key, group, ...args)\n      .then((raw) => parseXPendingCounts(raw));\n  }\n\n  xrange(\n    key: string,\n    start: XIdNeg,\n    end: XIdPos,\n    count?: number,\n  ) {\n    const args: (string | number)[] = [key, xidstr(start), xidstr(end)];\n    if (count !== undefined) {\n      args.push(\"COUNT\");\n      args.push(count);\n    }\n    return this.execArrayReply<XReadIdData>(\"XRANGE\", ...args).then(\n      (raw) => raw.map((m) => parseXMessage(m)),\n    );\n  }\n\n  xrevrange(\n    key: string,\n    start: XIdPos,\n    end: XIdNeg,\n    count?: number,\n  ) {\n    const args: (string | number)[] = [key, xidstr(start), xidstr(end)];\n    if (count !== undefined) {\n      args.push(\"COUNT\");\n      args.push(count);\n    }\n    return this.execArrayReply<XReadIdData>(\"XREVRANGE\", ...args).then(\n      (raw) => raw.map((m) => parseXMessage(m)),\n    );\n  }\n\n  xread(\n    keyXIds: (XKeyId | XKeyIdLike)[],\n    opts?: XReadOpts,\n  ) {\n    const args = [];\n    if (opts) {\n      if (opts.count !== undefined) {\n        args.push(\"COUNT\");\n        args.push(opts.count);\n      }\n      if (opts.block !== undefined) {\n        args.push(\"BLOCK\");\n        args.push(opts.block);\n      }\n    }\n    args.push(\"STREAMS\");\n\n    const theKeys = [];\n    const theXIds = [];\n\n    for (const a of keyXIds) {\n      if (a instanceof Array) {\n        // XKeyIdLike\n        theKeys.push(a[0]);\n        theXIds.push(xidstr(a[1]));\n      } else {\n        // XKeyId\n        theKeys.push(a.key);\n        theXIds.push(xidstr(a.xid));\n      }\n    }\n\n    return this.execArrayReply<XReadStreamRaw>(\n      \"XREAD\",\n      ...args.concat(theKeys).concat(theXIds),\n    ).then((raw) => parseXReadReply(raw));\n  }\n\n  xreadgroup(\n    keyXIds: (XKeyIdGroup | XKeyIdGroupLike)[],\n    { group, consumer, count, block }: XReadGroupOpts,\n  ) {\n    const args: (string | number)[] = [\n      \"GROUP\",\n      group,\n      consumer,\n    ];\n\n    if (count !== undefined) {\n      args.push(\"COUNT\");\n      args.push(count);\n    }\n    if (block !== undefined) {\n      args.push(\"BLOCK\");\n      args.push(block);\n    }\n\n    args.push(\"STREAMS\");\n\n    const theKeys = [];\n    const theXIds = [];\n\n    for (const a of keyXIds) {\n      if (a instanceof Array) {\n        // XKeyIdGroupLike\n        theKeys.push(a[0]);\n        theXIds.push(a[1] === \">\" ? \">\" : xidstr(a[1]));\n      } else {\n        // XKeyIdGroup\n        theKeys.push(a.key);\n        theXIds.push(a.xid === \">\" ? \">\" : xidstr(a.xid));\n      }\n    }\n\n    return this.execArrayReply<XReadStreamRaw>(\n      \"XREADGROUP\",\n      ...args.concat(theKeys).concat(theXIds),\n    ).then((raw) => parseXReadReply(raw));\n  }\n\n  xtrim(key: string, maxlen: XMaxlen) {\n    const args = [];\n    if (maxlen.approx) {\n      args.push(\"~\");\n    }\n\n    args.push(maxlen.elements);\n\n    return this.execIntegerReply(\"XTRIM\", key, \"MAXLEN\", ...args);\n  }\n\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    score: number,\n    member: string,\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    scoreMembers: [number, string][],\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zadd<TZAddOpts extends ZAddOpts = ZAddOpts>(\n    key: string,\n    memberScores: Record<string, number>,\n    opts?: TZAddOpts,\n  ): Promise<ZAddReply<TZAddOpts>>;\n  zadd(\n    key: string,\n    param1: number | [number, string][] | Record<string, number>,\n    param2?: string | ZAddOpts,\n    opts?: ZAddOpts,\n  ) {\n    const args: (string | number)[] = [key];\n    let isAbleToReturnNil = false;\n    if (Array.isArray(param1)) {\n      isAbleToReturnNil = this.pushZAddOpts(args, param2 as ZAddOpts);\n      args.push(...param1.flatMap((e) => e));\n      opts = param2 as ZAddOpts;\n    } else if (typeof param1 === \"object\") {\n      isAbleToReturnNil = this.pushZAddOpts(args, param2 as ZAddOpts);\n      for (const [member, score] of Object.entries(param1)) {\n        args.push(score as number, member);\n      }\n    } else {\n      isAbleToReturnNil = this.pushZAddOpts(args, opts);\n      args.push(param1, param2 as string);\n    }\n    return isAbleToReturnNil\n      ? this.execIntegerOrNilReply(\"ZADD\", ...args)\n      : this.execIntegerReply(\"ZADD\", ...args);\n  }\n\n  private pushZAddOpts(\n    args: (string | number)[],\n    opts?: ZAddOpts,\n  ): boolean {\n    let isAbleToReturnNil = false;\n    if (opts?.nx) {\n      args.push(\"NX\");\n      isAbleToReturnNil = true;\n    } else if (opts?.xx) {\n      args.push(\"XX\");\n      isAbleToReturnNil = true;\n    } else if (opts?.mode) {\n      args.push(opts.mode);\n      isAbleToReturnNil = true;\n    }\n    if (opts?.ch) {\n      args.push(\"CH\");\n    }\n    return isAbleToReturnNil;\n  }\n\n  zaddIncr(\n    key: string,\n    score: number,\n    member: string,\n    opts?: ZAddOpts,\n  ) {\n    const args: (string | number)[] = [key];\n    this.pushZAddOpts(args, opts);\n    args.push(\"INCR\", score, member);\n    return this.execBulkReply(\"ZADD\", ...args);\n  }\n\n  zcard(key: string) {\n    return this.execIntegerReply(\"ZCARD\", key);\n  }\n\n  zcount(key: string, min: number, max: number) {\n    return this.execIntegerReply(\"ZCOUNT\", key, min, max);\n  }\n\n  zincrby(key: string, increment: number, member: string) {\n    return this.execBulkReply<BulkString>(\"ZINCRBY\", key, increment, member);\n  }\n\n  zinter(\n    keys: string[] | [string, number][] | Record<string, number>,\n    opts?: ZInterOpts,\n  ) {\n    const args = this.pushZStoreArgs([], keys, opts);\n    if (opts?.withScore) {\n      args.push(\"WITHSCORES\");\n    }\n    return this.execArrayReply(\"ZINTER\", ...args);\n  }\n\n  zinterstore(\n    destination: string,\n    keys: string[] | [string, number][] | Record<string, number>,\n    opts?: ZInterstoreOpts,\n  ) {\n    const args = this.pushZStoreArgs([destination], keys, opts);\n    return this.execIntegerReply(\"ZINTERSTORE\", ...args);\n  }\n\n  zunionstore(\n    destination: string,\n    keys: string[] | [string, number][] | Record<string, number>,\n    opts?: ZUnionstoreOpts,\n  ) {\n    const args = this.pushZStoreArgs([destination], keys, opts);\n    return this.execIntegerReply(\"ZUNIONSTORE\", ...args);\n  }\n\n  private pushZStoreArgs(\n    args: (number | string)[],\n    keys: string[] | [string, number][] | Record<string, number>,\n    opts?: ZInterstoreOpts | ZUnionstoreOpts,\n  ) {\n    if (Array.isArray(keys)) {\n      args.push(keys.length);\n      if (Array.isArray(keys[0])) {\n        keys = keys as [string, number][];\n        args.push(...keys.map((e) => e[0]));\n        args.push(\"WEIGHTS\");\n        args.push(...keys.map((e) => e[1]));\n      } else {\n        args.push(...(keys as string[]));\n      }\n    } else {\n      args.push(Object.keys(keys).length);\n      args.push(...Object.keys(keys));\n      args.push(\"WEIGHTS\");\n      args.push(...Object.values(keys));\n    }\n    if (opts?.aggregate) {\n      args.push(\"AGGREGATE\", opts.aggregate);\n    }\n    return args;\n  }\n\n  zlexcount(key: string, min: string, max: string) {\n    return this.execIntegerReply(\"ZLEXCOUNT\", key, min, max);\n  }\n\n  zpopmax(key: string, count?: number) {\n    if (count !== undefined) {\n      return this.execArrayReply<BulkString>(\"ZPOPMAX\", key, count);\n    }\n    return this.execArrayReply<BulkString>(\"ZPOPMAX\", key);\n  }\n\n  zpopmin(key: string, count?: number) {\n    if (count !== undefined) {\n      return this.execArrayReply<BulkString>(\"ZPOPMIN\", key, count);\n    }\n    return this.execArrayReply<BulkString>(\"ZPOPMIN\", key);\n  }\n\n  zrange(\n    key: string,\n    start: number,\n    stop: number,\n    opts?: ZRangeOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, start, stop], opts);\n    return this.execArrayReply<BulkString>(\"ZRANGE\", ...args);\n  }\n\n  zrangebylex(\n    key: string,\n    min: string,\n    max: string,\n    opts?: ZRangeByLexOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, min, max], opts);\n    return this.execArrayReply<BulkString>(\"ZRANGEBYLEX\", ...args);\n  }\n\n  zrangebyscore(\n    key: string,\n    min: number | string,\n    max: number | string,\n    opts?: ZRangeByScoreOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, min, max], opts);\n    return this.execArrayReply<BulkString>(\"ZRANGEBYSCORE\", ...args);\n  }\n\n  zrank(key: string, member: string) {\n    return this.execIntegerOrNilReply(\"ZRANK\", key, member);\n  }\n\n  zrem(key: string, ...members: string[]) {\n    return this.execIntegerReply(\"ZREM\", key, ...members);\n  }\n\n  zremrangebylex(key: string, min: string, max: string) {\n    return this.execIntegerReply(\"ZREMRANGEBYLEX\", key, min, max);\n  }\n\n  zremrangebyrank(key: string, start: number, stop: number) {\n    return this.execIntegerReply(\"ZREMRANGEBYRANK\", key, start, stop);\n  }\n\n  zremrangebyscore(key: string, min: number | string, max: number | string) {\n    return this.execIntegerReply(\"ZREMRANGEBYSCORE\", key, min, max);\n  }\n\n  zrevrange(\n    key: string,\n    start: number,\n    stop: number,\n    opts?: ZRangeOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, start, stop], opts);\n    return this.execArrayReply<BulkString>(\"ZREVRANGE\", ...args);\n  }\n\n  zrevrangebylex(\n    key: string,\n    max: string,\n    min: string,\n    opts?: ZRangeByLexOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, min, max], opts);\n    return this.execArrayReply<BulkString>(\"ZREVRANGEBYLEX\", ...args);\n  }\n\n  zrevrangebyscore(\n    key: string,\n    max: number,\n    min: number,\n    opts?: ZRangeByScoreOpts,\n  ) {\n    const args = this.pushZRangeOpts([key, max, min], opts);\n    return this.execArrayReply<BulkString>(\"ZREVRANGEBYSCORE\", ...args);\n  }\n\n  private pushZRangeOpts(\n    args: (number | string)[],\n    opts?: ZRangeOpts | ZRangeByLexOpts | ZRangeByScoreOpts,\n  ) {\n    if ((opts as ZRangeByScoreOpts)?.withScore) {\n      args.push(\"WITHSCORES\");\n    }\n    if ((opts as ZRangeByScoreOpts)?.limit) {\n      args.push(\n        \"LIMIT\",\n        (opts as ZRangeByScoreOpts).limit!.offset,\n        (opts as ZRangeByScoreOpts).limit!.count,\n      );\n    }\n    return args;\n  }\n\n  zrevrank(key: string, member: string) {\n    return this.execIntegerOrNilReply(\"ZREVRANK\", key, member);\n  }\n\n  zscore(key: string, member: string) {\n    return this.execBulkReply(\"ZSCORE\", key, member);\n  }\n\n  scan(\n    cursor: number,\n    opts?: ScanOpts,\n  ) {\n    const args = this.pushScanOpts([cursor], opts);\n    return this.execArrayReply(\"SCAN\", ...args) as Promise<\n      [BulkString, BulkString[]]\n    >;\n  }\n\n  sscan(\n    key: string,\n    cursor: number,\n    opts?: SScanOpts,\n  ) {\n    const args = this.pushScanOpts([key, cursor], opts);\n    return this.execArrayReply(\"SSCAN\", ...args) as Promise<\n      [BulkString, BulkString[]]\n    >;\n  }\n\n  hscan(\n    key: string,\n    cursor: number,\n    opts?: HScanOpts,\n  ) {\n    const args = this.pushScanOpts([key, cursor], opts);\n    return this.execArrayReply(\"HSCAN\", ...args) as Promise<\n      [BulkString, BulkString[]]\n    >;\n  }\n\n  zscan(\n    key: string,\n    cursor: number,\n    opts?: ZScanOpts,\n  ) {\n    const args = this.pushScanOpts([key, cursor], opts);\n    return this.execArrayReply(\"ZSCAN\", ...args) as Promise<\n      [BulkString, BulkString[]]\n    >;\n  }\n\n  private pushScanOpts(\n    args: (number | string)[],\n    opts?: ScanOpts | HScanOpts | ZScanOpts | SScanOpts,\n  ) {\n    if (opts?.pattern !== undefined) {\n      args.push(\"MATCH\", opts.pattern);\n    }\n    if (opts?.count !== undefined) {\n      args.push(\"COUNT\", opts.count);\n    }\n    if ((opts as ScanOpts)?.type !== undefined) {\n      args.push(\"TYPE\", (opts as ScanOpts).type!);\n    }\n    return args;\n  }\n\n  latencyDoctor() {\n    return this.execBulkReply<BulkString>(\"LATENCY\", \"DOCTOR\");\n  }\n\n  tx() {\n    return createRedisPipeline(this.client.connection, true);\n  }\n\n  pipeline() {\n    return createRedisPipeline(this.client.connection);\n  }\n}\n\nexport interface RedisConnectOptions extends RedisConnectionOptions {\n  hostname: string;\n  port?: number | string;\n}\n\n/**\n * Connect to Redis server\n * @param options\n * @example\n * ```ts\n * import { connect } from \"./mod.ts\";\n * const conn1 = await connect({hostname: \"127.0.0.1\", port: 6379}); // -> TCP, 127.0.0.1:6379\n * const conn2 = await connect({hostname: \"redis.proxy\", port: 443, tls: true}); // -> TLS, redis.proxy:443\n * ```\n */\nexport async function connect(options: RedisConnectOptions): Promise<Redis> {\n  const { hostname, port, ...connectionOptions } = options;\n  const connection = createRedisConnection(hostname, port, connectionOptions);\n  await connection.connect();\n  const client = createDefaultClient(connection);\n  return create(client);\n}\n\n/**\n * Create a lazy Redis client that will not establish a connection until a command is actually executed.\n *\n * ```ts\n * import { createLazyClient } from \"./mod.ts\";\n *\n * const client = createLazyClient({ hostname: \"127.0.0.1\", port: 6379 });\n * console.assert(!client.isConnected);\n * await client.get(\"foo\");\n * console.assert(client.isConnected);\n * ```\n */\nexport function createLazyClient(options: RedisConnectOptions): Redis {\n  const { hostname, port, ...connectionOptions } = options;\n  const connection = createRedisConnection(hostname, port, connectionOptions);\n  const baseClient = createBaseLazyClient(connection);\n  return create(baseClient);\n}\n\n/**\n * Create {@linkcode Redis} from {@linkcode Client}.\n *\n * @deprecated This is an experimental API and may possibly be removed in the future.\n */\nexport function create(client: Client): Redis {\n  return new RedisImpl(client);\n}\n\n/**\n * Extract RedisConnectOptions from redis URL\n * @param url\n * @example\n * ```ts\n * import { parseURL } from \"./mod.ts\";\n *\n * parseURL(\"redis://foo:bar@localhost:6379/1\"); // -> {hostname: \"localhost\", port: \"6379\", tls: false, db: 1, name: foo, password: bar}\n * parseURL(\"rediss://127.0.0.1:443/?db=2&password=bar\"); // -> {hostname: \"127.0.0.1\", port: \"443\", tls: true, db: 2, name: undefined, password: bar}\n * ```\n */\nexport function parseURL(url: string): RedisConnectOptions {\n  const {\n    protocol,\n    hostname,\n    port,\n    username,\n    password,\n    pathname,\n    searchParams,\n  } = new URL(url);\n  const db = pathname.replace(\"/\", \"\") !== \"\"\n    ? pathname.replace(\"/\", \"\")\n    : searchParams.get(\"db\") ?? undefined;\n  return {\n    hostname: hostname !== \"\" ? hostname : \"localhost\",\n    port: port !== \"\" ? parseInt(port, 10) : 6379,\n    tls: protocol == \"rediss:\" ? true : searchParams.get(\"ssl\") === \"true\",\n    db: db ? parseInt(db, 10) : undefined,\n    name: username !== \"\" ? username : undefined,\n    password: password !== \"\"\n      ? password\n      : searchParams.get(\"password\") ?? undefined,\n  };\n}\n\nfunction createBaseLazyClient(connection: Connection): Client {\n  let client: Client | null = null;\n  async function ensureClient(): Promise<Client> {\n    if (!client) {\n      client = createDefaultClient(connection);\n      if (!connection.isConnected) {\n        await connection.connect();\n      }\n    }\n    return client;\n  }\n  return {\n    get connection() {\n      return connection;\n    },\n    exec(command, ...args) {\n      return this.sendCommand(command, args);\n    },\n    async subscribe(command, ...channelsOrPatterns) {\n      return (client ?? await ensureClient()).subscribe(\n        command,\n        ...channelsOrPatterns,\n      );\n    },\n    async sendCommand(command, args, options) {\n      const client = await ensureClient();\n      return client.sendCommand(command, args, options);\n    },\n    close() {\n      if (client) {\n        return client.close();\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "stream.ts",
    "content": "import type {\n  ConditionalArray,\n  Raw,\n  RedisValue,\n} from \"./protocol/shared/types.ts\";\n\nexport interface XId {\n  unixMs: number;\n  seqNo: number;\n}\n\nexport interface XMessage {\n  xid: XId;\n  fieldValues: Record<string, string>;\n}\n\nexport interface XKeyId {\n  key: string;\n  xid: XIdInput;\n}\n\nexport type XKeyIdLike = [string, XIdInput];\n\nexport interface XKeyIdGroup {\n  key: string;\n  xid: XIdGroupRead;\n}\n\nexport type XKeyIdGroupLike = [string, XIdGroupRead];\n\nexport type XReadStream = { key: string; messages: XMessage[] };\nexport type XReadReply = XReadStream[];\n\n// basic data returned by redis\nexport type XReadIdData = [string, string[]];\nexport type XReadStreamRaw = [string, XReadIdData[]];\nexport type XReadReplyRaw = XReadStreamRaw[];\n\n/** Flexible input type for commands which require message\n * ID to be passed (represented in lower-level Redis API as\n * \"1000-0\" etc).\n *\n * We also include an array format for ease of use, where\n * the first element is the epochMillis, second is seqNo.\n *\n * We also allow passing a single number,\n * which will represent the the epoch Millis with\n * seqNo of zero.  (Especially useful is to pass 0.)\n */\nexport type XIdInput = XId | [number, number] | number | string;\n/**\n * ID input type for XADD, which is allowed to include the\n * \"*\" operator. */\nexport type XIdAdd = XIdInput | \"*\";\n/**\n * ID input type for XGROUPREAD, which is allowed to include\n * the \">\" operator.  We include an array format for ease of\n * use, where the first element is the epochMillis, second\n * is seqNo. */\nexport type XIdGroupRead = XIdInput | \">\";\n\n/** Allows special maximum ID for XRANGE and XREVRANGE */\nexport type XIdPos = XIdInput | \"+\";\n/** Allows special minimum ID for XRANGE and XREVRANGE */\nexport type XIdNeg = XIdInput | \"-\";\n/** Allow special $ ID for XGROUP CREATE */\nexport type XIdCreateGroup = XIdInput | \"$\";\n\nexport type XAddFieldValues =\n  | Record<string | number, RedisValue>\n  | Map<string | number, RedisValue>;\n\nexport interface XReadOpts {\n  count?: number;\n  block?: number;\n}\n\nexport interface XReadGroupOpts {\n  group: string;\n  consumer: string;\n  count?: number;\n  block?: number;\n}\n\nexport interface XMaxlen {\n  approx?: boolean;\n  elements: number;\n}\n\nexport type XClaimReply = XClaimMessages | XClaimJustXId;\nexport interface XClaimMessages {\n  kind: \"messages\";\n  messages: XMessage[];\n}\nexport interface XClaimJustXId {\n  kind: \"justxid\";\n  xids: XId[];\n}\n\n/**\n * @param count Limit on the number of messages to return per call.\n * @param startId ID for the first pending record.\n * @param endId  ID for the final pending record.\n * @param consumers  Every consumer in the consumer group\n * with at least one pending message, and the number of\n * pending messages it has.\n */\nexport interface XPendingReply {\n  count: number;\n  startId: XId;\n  endId: XId;\n  consumers: XPendingConsumer[];\n}\nexport interface XPendingConsumer {\n  name: string;\n  pending: number;\n}\n\n/**\n * Represents a pending message parsed from xpending.\n *\n * @param id The ID of the message\n * @param consumer The name of the consumer that fetched the message\n *  and has still to acknowledge it. We call it the\n *  current owner of the message.\n * @param lastDeliveredMs The number of milliseconds that elapsed since the\n *  last time this message was delivered to this consumer.\n * @param timesDelivered The number of times this message was delivered.\n */\nexport interface XPendingCount {\n  xid: XId;\n  owner: string;\n  lastDeliveredMs: number;\n  timesDelivered: number;\n}\n/** Used in the XPENDING command, all three of these\n * args must be specified if _any_ are specified.\n */\nexport interface StartEndCount {\n  start: XIdNeg;\n  end: XIdPos;\n  count: number;\n}\n\nexport interface XInfoStreamReply {\n  length: number;\n  radixTreeKeys: number;\n  radixTreeNodes: number;\n  groups: number;\n  lastGeneratedId: XId;\n  firstEntry: XMessage;\n  lastEntry: XMessage;\n}\n\nexport interface XInfoStreamFullReply {\n  length: number;\n  radixTreeKeys: number;\n  radixTreeNodes: number;\n  lastGeneratedId: XId;\n  entries: XMessage[];\n  groups: XGroupDetail[];\n}\n\n/**\n * Child of the return type for xinfo_stream_full\n */\nexport interface XGroupDetail {\n  name: string;\n  lastDeliveredId: XId;\n  pelCount: number;\n  pending: XPendingCount[];\n  consumers: XConsumerDetail[];\n}\n/** Child of XINFO STREAMS FULL response */\nexport interface XConsumerDetail {\n  name: string;\n  seenTime: number;\n  pelCount: number;\n  pending: { xid: XId; lastDeliveredMs: number; timesDelivered: number }[];\n}\n\nexport type XInfoConsumersReply = XInfoConsumer[];\n/**\n * A consumer parsed from xinfo command.\n *\n * @param name Name of the consumer group.\n * @param pending Number of pending messages for this specific consumer.\n * @param idle This consumer's idle time in milliseconds.\n */\nexport interface XInfoConsumer {\n  name: string;\n  pending: number;\n  idle: number;\n}\n\n/** Response to XINFO GROUPS <key> */\nexport type XInfoGroupsReply = XInfoGroup[];\nexport interface XInfoGroup {\n  name: string;\n  consumers: number;\n  pending: number;\n  lastDeliveredId: XId;\n}\n\nexport interface XClaimOpts {\n  group: string;\n  consumer: string;\n  minIdleTime: number;\n  idle?: number;\n  time?: number;\n  retryCount?: number;\n  force?: boolean;\n  justXId?: boolean;\n}\n\nexport function parseXMessage(raw: XReadIdData): XMessage {\n  const fieldValues: Record<string, string> = {};\n  let f: string | undefined = undefined;\n\n  let m = 0;\n  for (const data of raw[1]) {\n    if (m % 2 === 0) {\n      f = data;\n    } else if (f) {\n      fieldValues[f] = data;\n    }\n    m++;\n  }\n\n  return { xid: parseXId(raw[0]), fieldValues: fieldValues };\n}\n\nexport function convertMap(raw: ConditionalArray): Map<string, Raw> {\n  const fieldValues: Map<string, Raw> = new Map();\n  let f: string | undefined = undefined;\n\n  let m = 0;\n  for (const data of raw) {\n    if (m % 2 === 0 && typeof data === \"string\") {\n      f = data;\n    } else if (m % 2 === 1 && f) {\n      fieldValues.set(f, data);\n    }\n    m++;\n  }\n\n  return fieldValues;\n}\n\nexport function parseXReadReply(raw: XReadReplyRaw): XReadReply {\n  const out: XReadStream[] = [];\n  for (const [key, idData] of raw ?? []) {\n    const messages = [];\n    for (const rawMsg of idData) {\n      messages.push(parseXMessage(rawMsg));\n    }\n    out.push({ key, messages });\n  }\n\n  return out;\n}\n\nexport function parseXId(raw: string): XId {\n  const [ms, sn] = raw.split(\"-\");\n  return { unixMs: parseInt(ms), seqNo: parseInt(sn) };\n}\n\nexport function parseXPendingConsumers(\n  raws: ConditionalArray,\n): XPendingConsumer[] {\n  const out: XPendingConsumer[] = [];\n\n  for (const raw of raws) {\n    if (isCondArray(raw) && isString(raw[0]) && isString(raw[1])) {\n      out.push({ name: raw[0], pending: parseInt(raw[1]) });\n    }\n  }\n\n  return out;\n}\n\nexport function parseXPendingCounts(raw: ConditionalArray): XPendingCount[] {\n  const infos: XPendingCount[] = [];\n  for (const r of raw) {\n    if (\n      isCondArray(r) &&\n      isString(r[0]) &&\n      isString(r[1]) &&\n      isNumber(r[2]) &&\n      isNumber(r[3])\n    ) {\n      infos.push({\n        xid: parseXId(r[0]),\n        owner: r[1],\n        lastDeliveredMs: r[2],\n        timesDelivered: r[3],\n      });\n    }\n  }\n\n  return infos;\n}\n\nexport function parseXGroupDetail(rawGroups: ConditionalArray): XGroupDetail[] {\n  const out = [];\n\n  for (const rawGroup of rawGroups) {\n    if (isCondArray(rawGroup)) {\n      const data = convertMap(rawGroup);\n\n      // array of arrays\n      const consDeets = data.get(\"consumers\") as ConditionalArray[];\n\n      out.push({\n        name: rawstr(data.get(\"name\") ?? null),\n        lastDeliveredId: parseXId(\n          rawstr(data.get(\"last-delivered-id\") ?? null),\n        ),\n        pelCount: rawnum(data.get(\"pel-count\") ?? null),\n        pending: parseXPendingCounts(data.get(\"pending\") as ConditionalArray),\n        consumers: parseXConsumerDetail(consDeets),\n      });\n    }\n  }\n\n  return out;\n}\n\nexport function parseXConsumerDetail(nestedRaws: Raw[][]): XConsumerDetail[] {\n  const out: XConsumerDetail[] = [];\n\n  for (const raws of nestedRaws) {\n    const data = convertMap(raws);\n\n    const pending = (data.get(\"pending\") as [string, number, number][]).map(\n      (p) => {\n        return {\n          xid: parseXId(rawstr(p[0])),\n          lastDeliveredMs: rawnum(p[1]),\n          timesDelivered: rawnum(p[2]),\n        };\n      },\n    );\n\n    const r = {\n      name: rawstr(data.get(\"name\") ?? null),\n      seenTime: rawnum(data.get(\"seen-time\") ?? null),\n      pelCount: rawnum(data.get(\"pel-count\") ?? null),\n      pending,\n    };\n\n    out.push(r);\n  }\n\n  return out;\n}\n\nexport function xidstr(\n  xid: XIdAdd | XIdNeg | XIdPos | XIdCreateGroup | XIdGroupRead,\n) {\n  if (typeof xid === \"string\") return xid;\n  if (typeof xid === \"number\") return `${xid}-0`;\n  if (xid instanceof Array && xid.length > 1) return `${xid[0]}-${xid[1]}`;\n  if (isXId(xid)) return `${xid.unixMs}-${xid.seqNo}`;\n  throw \"fail\";\n}\n\nexport function rawnum(raw: Raw): number {\n  return raw ? +raw.toString() : 0;\n}\nexport function rawstr(raw: Raw): string {\n  return raw ? raw.toString() : \"\";\n}\n// deno-lint-ignore no-explicit-any -- intended to be used as a type guard\nexport function isString(x: any): x is string {\n  return typeof x === \"string\";\n}\n\n// deno-lint-ignore no-explicit-any -- intended to be used as a type guard.\nexport function isNumber(x: any): x is number {\n  return typeof x === \"number\";\n}\n\nexport function isCondArray(x: Raw): x is ConditionalArray {\n  const l = (x as ConditionalArray).length;\n  if (l > 0 || l < 1) return true;\n  else return false;\n}\n\nfunction isXId(xid: XIdAdd): xid is XId {\n  return (xid as XId).unixMs !== undefined;\n}\n"
  },
  {
    "path": "subscription.ts",
    "content": "import type { Binary } from \"./protocol/shared/types.ts\";\nexport type DefaultPubSubMessageType = string;\nexport type PubSubMessageType = string | string[];\nexport type SubscribeCommand = \"SUBSCRIBE\" | \"PSUBSCRIBE\";\n\nexport interface RedisPubSubMessage<TMessage = DefaultPubSubMessageType> {\n  pattern?: string;\n  channel: string;\n  message: TMessage;\n}\n\nexport interface RedisSubscription<\n  TMessage extends PubSubMessageType = DefaultPubSubMessageType,\n> {\n  readonly isClosed: boolean;\n  receive(): AsyncIterableIterator<RedisPubSubMessage<TMessage>>;\n  receiveBuffers(): AsyncIterableIterator<RedisPubSubMessage<Binary>>;\n  psubscribe(...patterns: string[]): Promise<void>;\n  subscribe(...channels: string[]): Promise<void>;\n  punsubscribe(...patterns: string[]): Promise<void>;\n  unsubscribe(...channels: string[]): Promise<void>;\n  close(): void;\n}\n"
  },
  {
    "path": "tests/backoff_test.ts",
    "content": "import { assertEquals } from \"../deps/std/assert.ts\";\nimport { describe, it } from \"../deps/std/testing.ts\";\n\nimport { exponentialBackoff } from \"../backoff.ts\";\n\ndescribe(\"backoff\", {\n  permissions: \"none\",\n}, () => {\n  describe(\"exponentialBackoff\", () => {\n    it(\"should return exponentially increasing backoff intervals\", () => {\n      const backoff = exponentialBackoff({\n        multiplier: 2,\n        maxInterval: 5000,\n        minInterval: 1000,\n      });\n\n      assertEquals(backoff(1), 1000);\n      assertEquals(backoff(2), 2000);\n      assertEquals(backoff(3), 4000);\n      assertEquals(backoff(4), 5000);\n      assertEquals(backoff(5), 5000);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/client_test.ts",
    "content": "import type { Redis } from \"../redis.ts\";\nimport { delay } from \"../deps/std/async.ts\";\nimport {\n  assert,\n  assertEquals,\n  assertRejects,\n  assertThrows,\n} from \"../deps/std/assert.ts\";\nimport {\n  afterAll,\n  afterEach,\n  beforeAll,\n  beforeEach,\n  describe,\n  it,\n} from \"../deps/std/testing.ts\";\nimport { newClient, nextPort, startRedis, stopRedis } from \"./test_util.ts\";\nimport type { TestServer } from \"./test_util.ts\";\n\ndescribe(\"client\", () => {\n  let port!: number;\n  let server!: TestServer;\n  let client!: Redis;\n\n  beforeAll(async () => {\n    port = nextPort();\n    server = await startRedis({ port });\n  });\n\n  afterAll(() => stopRedis(server));\n\n  beforeEach(async () => {\n    client = await newClient({ hostname: \"127.0.0.1\", port });\n  });\n\n  afterEach(() => client.close());\n\n  it(\"client caching with opt in\", async () => {\n    await client.clientTracking({ mode: \"ON\", optIn: true });\n    assertEquals(await client.clientCaching(\"YES\"), \"OK\");\n  });\n\n  it(\"client caching with opt out\", async () => {\n    await client.clientTracking({ mode: \"ON\", optOut: true });\n    assertEquals(await client.clientCaching(\"NO\"), \"OK\");\n  });\n\n  it(\"client caching without opt in or opt out\", async () => {\n    await assertRejects(\n      () => {\n        return client.clientCaching(\"YES\");\n      },\n      Error,\n      \"-ERR CLIENT CACHING can be called only when the client is in tracking mode with OPTIN or OPTOUT mode enabled\",\n    );\n  });\n\n  it(\"client id\", async () => {\n    const id = await client.clientID();\n    assertEquals(typeof id, \"number\");\n  });\n\n  it(\"client info\", async () => {\n    const id = await client.clientID();\n    const info = await client.clientInfo();\n    assert(info!.includes(`id=${id}`));\n  });\n\n  it(\"client setname & getname\", async () => {\n    assertEquals(await client.clientSetName(\"deno-redis\"), \"OK\");\n    assertEquals(await client.clientGetName(), \"deno-redis\");\n  });\n\n  it(\"client getredir with no redirect\", async () => {\n    assertEquals(await client.clientGetRedir(), -1);\n  });\n\n  it(\"client getredir with redirect\", async () => {\n    const tempClient = await newClient({ hostname: \"127.0.0.1\", port });\n    try {\n      const id = await tempClient.clientID();\n      await client.clientTracking({ mode: \"ON\", redirect: id });\n      assertEquals(await client.clientGetRedir(), id);\n    } finally {\n      tempClient.close();\n    }\n  });\n\n  it(\"client pause & unpause\", async () => {\n    assertEquals(await client.clientPause(5), \"OK\");\n    assertEquals(await client.clientPause(5, \"ALL\"), \"OK\");\n    assertEquals(await client.clientPause(5, \"WRITE\"), \"OK\");\n    assertEquals(await client.clientUnpause(), \"OK\");\n  });\n\n  it(\"client kill by addr\", async () => {\n    const tempClient = await newClient({ hostname: \"127.0.0.1\", port });\n    try {\n      const info = await client.clientInfo() as string;\n      const addr = info.split(\" \").find((s) =>\n        s.startsWith(\"addr=\")\n      )!.split(\"=\")[1];\n      assertEquals(await tempClient.clientKill({ addr }), 1);\n    } finally {\n      tempClient.close();\n    }\n  });\n\n  it(\"client kill by id\", async () => {\n    const tempClient = await newClient({ hostname: \"127.0.0.1\", port });\n    try {\n      const id = await client.clientID();\n      assertEquals(await tempClient.clientKill({ id }), 1);\n    } finally {\n      tempClient.close();\n    }\n  });\n\n  it(\"client list\", async () => {\n    const id = await client.clientID();\n    let list = await client.clientList();\n    assert(list!.includes(`id=${id}`));\n\n    list = await client.clientList({ type: \"PUBSUB\" });\n    assertEquals(list, \"\");\n\n    list = await client.clientList({ type: \"NORMAL\" });\n    assert(list!.includes(`id=${id}`));\n\n    list = await client.clientList({ ids: [id] });\n    assert(list!.includes(`id=${id}`));\n\n    assertThrows(\n      () => {\n        return client.clientList({ type: \"MASTER\", ids: [id] });\n      },\n      Error,\n      \"only one of `type` or `ids` can be specified\",\n    );\n  });\n\n  it(\"client tracking\", async () => {\n    assertEquals(\n      await client.clientTracking({\n        mode: \"ON\",\n        prefixes: [\"foo\", \"bar\"],\n        bcast: true,\n      }),\n      \"OK\",\n    );\n    assertEquals(\n      await client.clientTracking({\n        mode: \"ON\",\n        bcast: true,\n        optIn: false,\n        noLoop: true,\n      }),\n      \"OK\",\n    );\n    await assertRejects(\n      () => {\n        return client.clientTracking({ mode: \"ON\", bcast: true, optIn: true });\n      },\n      Error,\n      \"-ERR OPTIN and OPTOUT are not compatible with BCAST\",\n    );\n  });\n\n  it(\"client trackinginfo\", async () => {\n    const info = await client.clientTrackingInfo();\n    assert(info.includes(\"flags\"));\n    assert(info.includes(\"redirect\"));\n    assert(info.includes(\"prefixes\"));\n  });\n\n  it(\"client unblock nothing\", async () => {\n    const id = await client.clientID();\n    assertEquals(await client.clientUnblock(id), 0);\n  });\n\n  it(\"client unblock with timeout\", async () => {\n    const tempClient = await newClient({ hostname: \"127.0.0.1\", port });\n    try {\n      const id = await tempClient.clientID();\n      const promise = tempClient.brpop(0, \"key1\"); // Block.\n      await delay(5); // Give some leeway for brpop to reach redis.\n      assertEquals(await client.clientUnblock(id, \"TIMEOUT\"), 1);\n      await promise;\n    } finally {\n      tempClient.close();\n    }\n  });\n\n  it(\"client unblock with error\", async () => {\n    const tempClient = await newClient({ hostname: \"127.0.0.1\", port });\n    try {\n      const id = await tempClient.clientID();\n      const promise = assertRejects(\n        () => tempClient.brpop(0, \"key1\"),\n        Error,\n        \"-UNBLOCKED\",\n      );\n      await delay(5); // Give some leeway for brpop to reach redis.\n      assertEquals(await client.clientUnblock(id, \"ERROR\"), 1);\n      await promise;\n    } finally {\n      tempClient.close();\n    }\n  });\n\n  it(\"client kill by type and don't skip ourselves\", async () => {\n    assertEquals(await client.clientKill({ type: \"NORMAL\", skipme: \"NO\" }), 1);\n  });\n});\n"
  },
  {
    "path": "tests/cluster/test.ts",
    "content": "import { nextPorts, startRedisCluster, stopRedisCluster } from \"./test_util.ts\";\nimport type { TestCluster } from \"./test_util.ts\";\nimport { connect as connectToCluster } from \"../../experimental/cluster/mod.ts\";\nimport {\n  assert,\n  assertArrayIncludes,\n  assertEquals,\n  assertRejects,\n} from \"../../deps/std/assert.ts\";\nimport { sample } from \"../../deps/std/random.ts\";\nimport {\n  afterAll,\n  afterEach,\n  beforeAll,\n  describe,\n  it,\n} from \"../../deps/std/testing.ts\";\nimport { calculateSlot } from \"../../deps/cluster-key-slot.js\";\nimport { ErrorReplyError } from \"../../errors.ts\";\nimport { connect, create } from \"../../redis.ts\";\nimport type { CommandExecutor } from \"../../executor.ts\";\nimport type { Connection } from \"../../connection.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\ndescribe(\"experimental/cluster\", () => {\n  let ports!: Array<number>;\n  let cluster!: TestCluster;\n  let nodes!: Array<{ hostname: string; port: number }>;\n  let client!: Redis;\n\n  beforeAll(async () => {\n    ports = nextPorts(6);\n    cluster = await startRedisCluster(ports);\n    nodes = ports.map((port) => ({\n      hostname: \"127.0.0.1\",\n      port,\n    }));\n    client = await connectToCluster({ nodes });\n  });\n\n  afterAll(() => stopRedisCluster(cluster));\n\n  afterEach(() => client.close());\n\n  it(\"del multiple keys in the same hash slot\", async () => {\n    await client.set(\"{hoge}foo\", \"a\");\n    await client.set(\"{hoge}bar\", \"b\");\n    const r = await client.del(\"{hoge}foo\", \"{hoge}bar\");\n    assertEquals(r, 2);\n  });\n\n  it(\"del multiple keys in different hash slots\", async () => {\n    await client.set(\"foo\", \"a\");\n    await client.set(\"bar\", \"b\");\n    await assertRejects(\n      async () => {\n        await client.del(\"foo\", \"bar\");\n      },\n      ErrorReplyError,\n      \"-CROSSSLOT Keys in request don't hash to the same slot\",\n    );\n  });\n\n  it(\"handle a -MOVED redirection error\", async () => {\n    let redirected = false;\n    let manuallyRedirectedPort!: number;\n    const portsSent = new Set<number>();\n    const client = await connectToCluster({\n      nodes,\n      async newRedis(opts) {\n        const redis = await connect(opts);\n        assert(opts.port != null);\n        const proxyExecutor = {\n          get connection(): Connection {\n            throw new Error(\"Not supported\");\n          },\n          close() {\n            return redis.close();\n          },\n          exec(cmd, ...args) {\n            return this.sendCommand(cmd, args);\n          },\n          async sendCommand(cmd, args, options) {\n            if (cmd === \"GET\" && !redirected) {\n              // Manually cause a -MOVED redirection error\n              const [key] = args ?? [];\n              assert(typeof key === \"string\");\n              const slot = calculateSlot(key);\n              manuallyRedirectedPort = sample(\n                ports.filter((x) => x !== opts.port),\n              )!;\n              const error = new ErrorReplyError(\n                `-MOVED ${slot} ${opts.hostname}:${manuallyRedirectedPort}`,\n              );\n              redirected = true;\n              throw error;\n            } else {\n              assert(opts.port);\n              portsSent.add(Number(opts.port));\n              const reply = await redis.sendCommand(cmd, args, options);\n              return reply;\n            }\n          },\n        } as CommandExecutor;\n        return create(proxyExecutor);\n      },\n    });\n\n    try {\n      await client.set(\"foo\", \"bar\");\n      const r = await client.get(\"foo\");\n      assertEquals(r, \"bar\");\n      // Check if a cluster client correctly handles a -MOVED error\n      assert(redirected);\n      assertArrayIncludes<number>([...portsSent], [manuallyRedirectedPort]);\n    } finally {\n      client.close();\n    }\n  });\n\n  it(\"handle a -ASK redirection error\", async () => {\n    let redirected = false;\n    let manuallyRedirectedPort!: number;\n    const portsSent = new Set<number>();\n    const commandsSent = new Set<string>();\n    const client = await connectToCluster({\n      nodes,\n      async newRedis(opts) {\n        const redis = await connect(opts);\n        assert(opts.port != null);\n        const proxyExecutor = {\n          get connection(): Connection {\n            throw new Error(\"Not supported\");\n          },\n          close() {\n            return redis.close();\n          },\n          exec(cmd, ...args) {\n            return this.sendCommand(cmd, args);\n          },\n          async sendCommand(cmd, args, options) {\n            commandsSent.add(cmd);\n            if (cmd === \"GET\" && !redirected) {\n              // Manually cause a -ASK redirection error\n              const [key] = args ?? [];\n              assert(typeof key === \"string\");\n              const slot = calculateSlot(key);\n              manuallyRedirectedPort = sample(\n                ports.filter((x) => x !== opts.port),\n              )!;\n              const error = new ErrorReplyError(\n                `-ASK ${slot} ${opts.hostname}:${manuallyRedirectedPort}`,\n              );\n              redirected = true;\n              throw error;\n            } else {\n              assert(opts.port);\n              portsSent.add(Number(opts.port));\n              const reply = await redis.sendCommand(cmd, args, options);\n              return reply;\n            }\n          },\n        } as CommandExecutor;\n        return create(proxyExecutor);\n      },\n    });\n    try {\n      await client.set(\"hoge\", \"piyo\");\n      const r = await client.get(\"hoge\");\n      assertEquals(r, \"piyo\");\n      // Check if a cluster client correctly handles a -ASK error\n      assert(redirected);\n      assertArrayIncludes<number>([...portsSent], [manuallyRedirectedPort]);\n      assertArrayIncludes<string>([...commandsSent], [\"ASKING\"]);\n    } finally {\n      client.close();\n    }\n  });\n\n  it(\"properly handle too many redirections\", async () => {\n    const client = await connectToCluster({\n      nodes,\n      async newRedis(opts) {\n        const redis = await connect(opts);\n        assert(opts.port != null);\n        const proxyExecutor = {\n          get connection(): Connection {\n            throw new Error(\"Not supported\");\n          },\n          close() {\n            return redis.close();\n          },\n          exec(cmd, ...args) {\n            return this.sendCommand(cmd, args);\n          },\n          async sendCommand(cmd, args, options) {\n            if (cmd === \"GET\") {\n              // Manually cause a -MOVED redirection error\n              const [key] = args ?? [];\n              assert(typeof key === \"string\");\n              const slot = calculateSlot(key);\n              const randomPort = sample(\n                ports.filter((x) => x !== opts.port),\n              );\n              const error = new ErrorReplyError(\n                `-MOVED ${slot} ${opts.hostname}:${randomPort}`,\n              );\n              throw error;\n            } else {\n              const reply = await redis.sendCommand(cmd, args, options);\n              return reply;\n            }\n          },\n        } as CommandExecutor;\n        return create(proxyExecutor);\n      },\n    });\n    try {\n      await assertRejects(\n        () => client.get(\"foo\"),\n        Error,\n        \"Too many Cluster redirections?\",\n      );\n    } finally {\n      client.close();\n    }\n  });\n});\n"
  },
  {
    "path": "tests/cluster/test_util.ts",
    "content": "import {\n  ensureTerminated,\n  nextPort,\n  startRedis,\n  stopRedis,\n} from \"../test_util.ts\";\nimport type { TestServer } from \"../test_util.ts\";\nimport { readAll, readerFromStreamReader } from \"../../deps/std/io.ts\";\nimport { delay } from \"../../deps/std/async.ts\";\n\nexport interface TestCluster {\n  servers: TestServer[];\n}\n\nexport async function startRedisCluster(ports: number[]): Promise<TestCluster> {\n  const servers = await Promise.all(ports.map((port) =>\n    startRedis({\n      port,\n      clusterEnabled: true,\n      makeClusterConfigFile: true,\n    })\n  ));\n  const cluster = { servers };\n  const redisCLI = new Deno.Command(\"redis-cli\", {\n    args: [\n      \"--cluster\",\n      \"create\",\n      ...ports.map((port) => `127.0.0.1:${port}`),\n      \"--cluster-replicas\",\n      \"1\",\n      \"--cluster-yes\",\n    ],\n    stderr: \"piped\",\n  }).spawn();\n  try {\n    // Wait for cluster setup to complete...\n    const status = await redisCLI.status;\n    if (!status.success) {\n      stopRedisCluster(cluster);\n      const errOutput = await readAll(\n        readerFromStreamReader(redisCLI.stderr.getReader()),\n      );\n      const decoder = new TextDecoder();\n      throw new Error(`Failed to setup cluster: ${decoder.decode(errOutput)}`);\n    }\n\n    // Ample time for cluster to finish startup\n    await delay(5000);\n\n    return cluster;\n  } finally {\n    ensureTerminated(redisCLI);\n  }\n}\n\nexport async function stopRedisCluster(cluster: TestCluster): Promise<void> {\n  for (const server of cluster.servers) {\n    await stopRedis(server);\n  }\n}\n\nexport function nextPorts(n: number): Array<number> {\n  return Array(n).fill(0).map(() => nextPort());\n}\n"
  },
  {
    "path": "tests/cluster_test.ts",
    "content": "import type { Redis } from \"../mod.ts\";\nimport {\n  assert,\n  assertEquals,\n  assertStringIncludes,\n} from \"../deps/std/assert.ts\";\nimport { afterAll, beforeAll, describe, it } from \"../deps/std/testing.ts\";\nimport { newClient, nextPort, startRedis, stopRedis } from \"./test_util.ts\";\nimport type { TestServer } from \"./test_util.ts\";\n\ndescribe(\"cluster\", () => {\n  let port1!: number;\n  let port2!: number;\n  let s1!: TestServer;\n  let s2!: TestServer;\n  let client!: Redis;\n\n  beforeAll(async () => {\n    port1 = nextPort();\n    port2 = nextPort();\n    s1 = await startRedis({ port: port1, clusterEnabled: true });\n    s2 = await startRedis({ port: port2, clusterEnabled: true });\n    client = await newClient({ hostname: \"127.0.0.1\", port: port2 });\n  });\n\n  afterAll(async () => {\n    await stopRedis(s1);\n    await stopRedis(s2);\n    client.close();\n  });\n\n  it(\"addslots\", async () => {\n    await client.clusterFlushSlots();\n    assertEquals(await client.clusterAddSlots(1, 2, 3), \"OK\");\n  });\n\n  it(\"myid\", async () => {\n    assert(!!(await client.clusterMyID()));\n  });\n\n  it(\"countfailurereports\", async () => {\n    const nodeId = await client.clusterMyID();\n    assertEquals(await client.clusterCountFailureReports(nodeId), 0);\n  });\n\n  it(\"countkeysinslot\", async () => {\n    assertEquals(await client.clusterCountKeysInSlot(1), 0);\n  });\n\n  it(\"delslots\", async () => {\n    assertEquals(await client.clusterDelSlots(1, 2, 3), \"OK\");\n  });\n\n  it(\"getkeysinslot\", async () => {\n    assertEquals(await client.clusterGetKeysInSlot(1, 1), []);\n  });\n\n  it(\"flushslots\", async () => {\n    assertEquals(await client.clusterFlushSlots(), \"OK\");\n  });\n\n  it(\"info\", async () => {\n    assertStringIncludes(await client.clusterInfo(), \"cluster_state\");\n  });\n\n  it(\"keyslot\", async () => {\n    assertEquals(await client.clusterKeySlot(\"somekey\"), 11058);\n  });\n\n  it(\"meet\", async () => {\n    assertEquals(await client.clusterMeet(\"127.0.0.1\", port2), \"OK\");\n  });\n\n  it(\"nodes\", async () => {\n    const nodeId = await client.clusterMyID();\n    const nodes = await client.clusterNodes();\n    assertStringIncludes(nodes, nodeId);\n  });\n\n  it(\"replicas\", async () => {\n    const nodeId = await client.clusterMyID();\n    assertEquals(await client.clusterReplicas(nodeId), []);\n  });\n\n  it(\"slaves\", async () => {\n    const nodeId = await client.clusterMyID();\n    assertEquals(await client.clusterSlaves(nodeId), []);\n  });\n\n  it(\"forget\", async () => {\n    const nodeId = await client.clusterMyID();\n    const otherNode = (await client.clusterNodes())\n      .split(\"\\n\")\n      .find((n) => !n.startsWith(nodeId))\n      ?.split(\" \")[0];\n    if (otherNode) {\n      assertEquals(await client.clusterForget(otherNode), \"OK\");\n    }\n  });\n\n  it(\"saveconfig\", async () => {\n    assertEquals(await client.clusterSaveConfig(), \"OK\");\n  });\n\n  it(\"setslot\", async () => {\n    const nodeId = await client.clusterMyID();\n    assertEquals(await client.clusterSetSlot(1, \"NODE\", nodeId), \"OK\");\n    assertEquals(await client.clusterSetSlot(1, \"MIGRATING\", nodeId), \"OK\");\n    assertEquals(await client.clusterSetSlot(1, \"STABLE\"), \"OK\");\n  });\n\n  it(\"slots\", async () => {\n    assert(Array.isArray(await client.clusterSlots()));\n  });\n\n  it(\"replicate\", async () => {\n    const nodeId = await client.clusterMyID();\n    const otherNode = (await client.clusterNodes())\n      .split(\"\\n\")\n      .find((n) => !n.startsWith(nodeId))\n      ?.split(\" \")[0];\n    if (otherNode) {\n      assertEquals(await client.clusterReplicate(otherNode), \"OK\");\n    }\n  });\n\n  it(\"failover\", async () => {\n    const nodeId = await client.clusterMyID();\n    const otherNode = (await client.clusterNodes())\n      .split(\"\\n\")\n      .find((n) => !n.startsWith(nodeId))\n      ?.split(\" \")[0];\n    if (otherNode) {\n      assertEquals(await client.clusterFailover(), \"OK\");\n    }\n  });\n\n  it(\"reset\", async () => {\n    assertEquals(await client.clusterReset(), \"OK\");\n  });\n});\n"
  },
  {
    "path": "tests/commands/acl.ts",
    "content": "import {\n  assertArrayIncludes,\n  assertEquals,\n  assertStringIncludes,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, describe, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport { usesRedisVersion } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function aclTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  describe(\"whoami\", () => {\n    it(\"returns the username of the current connection\", async () => {\n      assertEquals(await client.aclWhoami(), \"default\");\n    });\n  });\n\n  describe(\"list\", () => {\n    it(\"returns the ACL rules\", async () => {\n      const rules = await client.aclList();\n      assertStringIncludes(rules[0], \"user default on nopass\");\n      assertEquals(rules.length, 1);\n    });\n  });\n\n  describe(\"getuser\", () => {\n    it(\"returns the user's ACL flags\", async () => {\n      const flags = await client.aclGetUser(\"default\");\n      assertArrayIncludes(flags, [\n        \"flags\",\n        \"passwords\",\n        \"commands\",\n        \"channels\",\n      ]);\n    });\n  });\n\n  describe(\"cat\", () => {\n    it(\"returns the available ACL categories if no arguments are given\", async () => {\n      assertArrayIncludes(\n        (await client.aclCat()).sort(),\n        [\n          \"keyspace\",\n          \"read\",\n          \"write\",\n          \"set\",\n          \"sortedset\",\n          \"list\",\n          \"hash\",\n          \"string\",\n          \"bitmap\",\n          \"hyperloglog\",\n          \"geo\",\n          \"stream\",\n          \"pubsub\",\n          \"admin\",\n          \"fast\",\n          \"slow\",\n          \"blocking\",\n          \"dangerous\",\n          \"connection\",\n          \"transaction\",\n          \"scripting\",\n        ].sort(),\n      );\n    });\n\n    it(\"returns the commands in the specified category\", async () => {\n      assertArrayIncludes(\n        (await client.aclCat(\"dangerous\")).sort(),\n        [\n          \"lastsave\",\n          \"shutdown\",\n          \"monitor\",\n          \"role\",\n          \"replconf\",\n          \"pfselftest\",\n          \"save\",\n          \"replicaof\",\n          \"restore-asking\",\n          \"restore\",\n          \"swapdb\",\n          \"slaveof\",\n          \"bgsave\",\n          \"debug\",\n          \"bgrewriteaof\",\n          \"sync\",\n          \"flushdb\",\n          \"keys\",\n          \"psync\",\n          \"pfdebug\",\n          \"flushall\",\n          \"failover\",\n          \"info\",\n          \"migrate\",\n        ],\n      );\n    });\n  });\n\n  describe(\"users\", () => {\n    it(\"returns usernames\", async () => {\n      assertEquals(await client.aclUsers(), [\"default\"]);\n    });\n  });\n\n  describe(\"setuser\", () => {\n    it(\"returns `OK` on success\", async () => {\n      assertEquals(await client.aclSetUser(\"alan\", \"+get\"), \"OK\");\n      assertEquals(await client.aclDelUser(\"alan\"), 1);\n    });\n  });\n\n  describe(\"deluser\", () => {\n    it(\"returns the number of deleted users\", async () => {\n      assertEquals(await client.aclDelUser(\"alan\"), 0);\n    });\n  });\n\n  describe(\"genpass\", () => {\n    it(\"returns the generated password\", async () => {\n      const reply = await client.aclGenPass();\n      assertEquals(typeof reply, \"string\");\n      assertEquals(reply.length, 64);\n\n      const testlen = 32;\n      assertEquals((await client.aclGenPass(testlen)).length, testlen / 4);\n    });\n  });\n\n  describe(\"auth\", () => {\n    it(\"returns `OK` on success\", async () => {\n      assertEquals(await client.auth(\"default\", \"\"), \"OK\");\n    });\n  });\n\n  describe(\"log\", () => {\n    const randString = \"balh\";\n    beforeAll(async () => {\n      try {\n        await client.auth(randString, randString);\n      } catch (_error) {\n        // skip invalid username-password pair error\n      }\n    });\n\n    it(\"returns the ACL security events\", async () => {\n      assertEquals((await client.aclLog(1))[0][9], randString);\n    });\n\n    it(\"returns `OK` when called with `RESET`\", async () => {\n      assertEquals(await client.aclLog(\"RESET\"), \"OK\");\n    });\n  });\n\n  describe(\"module list\", () => {\n    it(\n      \"returns an empty array by default\",\n      { ignore: usesRedisVersion(\"8\") },\n      async () => {\n        assertEquals(await client.moduleList(), []);\n      },\n    );\n\n    it(\n      \"returns `vectorset` module by default\",\n      { ignore: !usesRedisVersion(\"8\") },\n      async () => {\n        const moduleList = await client.moduleList();\n        assertStringIncludes(JSON.stringify(moduleList[0]), \"vectorset\");\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "tests/commands/connection.ts",
    "content": "import { createLazyClient } from \"../../mod.ts\";\nimport {\n  assert,\n  assertArrayIncludes,\n  assertEquals,\n  assertExists,\n  assertInstanceOf,\n  assertRejects,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, describe, it } from \"../../deps/std/testing.ts\";\nimport { delay } from \"../../deps/std/async.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function connectionTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  const getOpts = () => ({\n    hostname: \"127.0.0.1\",\n    port: getServer().port,\n  });\n  beforeAll(async () => {\n    client = await connect(getOpts());\n  });\n\n  afterAll(() => client.close());\n\n  describe(\"echo\", () => {\n    it(\"returns `message` as-is\", async () => {\n      assertEquals(await client.echo(\"Hello World\"), \"Hello World\");\n    });\n  });\n\n  describe(\"ping\", () => {\n    it(\"returns `PONG` if no argument is given\", async () => {\n      assertEquals(await client.ping(), \"PONG\");\n    });\n\n    it(\"returns `message` as-is\", async () => {\n      assertEquals(await client.ping(\"Deno\"), \"Deno\");\n    });\n  });\n\n  describe(\"quit\", () => {\n    it(\"closes the connection\", async () => {\n      const { port } = getServer();\n      const tempClient = await connect({ hostname: \"127.0.0.1\", port });\n      assertEquals(tempClient.isConnected, true);\n      assertEquals(tempClient.isClosed, false);\n      assertEquals(await tempClient.quit(), \"OK\");\n      assertEquals(tempClient.isConnected, false);\n      assertEquals(tempClient.isClosed, true);\n    });\n  });\n\n  describe(\"select\", () => {\n    it(\"returns `OK` on success\", async () => {\n      assertEquals(await client.select(1), \"OK\");\n    });\n  });\n\n  describe(\"hello\", () => {\n    it(\"works with no args\", async () => {\n      const reply = await client.hello();\n      assertArrayIncludes(reply, [\"redis\"]);\n    });\n\n    it(\"supports AUTH\", async () => {\n      const reply = await client.hello({\n        protover: 2,\n        auth: { username: \"default\", password: \"\" },\n      });\n      assertArrayIncludes(reply, [\"redis\"]);\n    });\n\n    it(\"supports SETNAME\", async () => {\n      await client.hello({\n        protover: 2,\n        clientName: \"deno-redis\",\n      });\n      assertEquals(await client.clientGetName(), \"deno-redis\");\n    });\n  });\n\n  describe(\"swapdb\", () => {\n    it(\"returns `OK` on success\", async () => {\n      assertEquals(await client.swapdb(0, 1), \"OK\");\n    });\n  });\n\n  describe(\"health check\", () => {\n    it(\"should send a ping every `healthCheckInterval`\", async () => {\n      const opts = {\n        ...getOpts(),\n        healthCheckInterval: 10,\n      };\n      const client = await connect(opts);\n      const rawPreviousCommandStats = await client.info(\"commandstats\");\n      await delay(25);\n      const rawCurrentCommandStats = await client.info(\"commandstats\");\n      client.close();\n\n      await delay(10); // NOTE: After closing the connection, no errors should occur\n\n      const previousPingStats =\n        parseCommandStats(rawPreviousCommandStats)[\"ping\"];\n      const currentPingStats =\n        parseCommandStats(rawCurrentCommandStats)[\"ping\"];\n      assertExists(previousPingStats);\n      assertExists(currentPingStats);\n\n      const previousCallCount = previousPingStats[\"calls\"];\n      const currentCallCount = currentPingStats[\"calls\"];\n      const d = currentCallCount - previousCallCount;\n      assert(d >= 2, `${d} should be greater than or equal to 2`);\n    });\n  });\n\n  describe(\"createLazyClient\", () => {\n    it(\"returns the lazily connected client\", async () => {\n      const opts = getOpts();\n      const client = createLazyClient(opts);\n      assert(!client.isConnected);\n      try {\n        await client.get(\"foo\");\n        assert(client.isConnected);\n      } finally {\n        client.close();\n      }\n    });\n  });\n\n  describe(\"connect()\", () => {\n    it(\"connects to the server\", async () => {\n      const client = await connect(getOpts());\n      assert(client.isConnected);\n\n      client.close();\n      assert(!client.isConnected);\n\n      await client.connect();\n      assert(client.isConnected);\n\n      assertEquals(await client.ping(), \"PONG\");\n\n      client.close();\n    });\n\n    it(\"supports AbortSignal\", async () => {\n      const ac = new AbortController();\n      ac.abort();\n      const error = await assertRejects(async () =>\n        await connect({\n          ...getOpts(),\n          signal: () => ac.signal,\n        }), DOMException);\n      assertEquals(error.name, \"AbortError\");\n    });\n\n    it(\"works with a lazy client\", async () => {\n      const client = createLazyClient(getOpts());\n      assert(!client.isConnected);\n\n      await client.connect();\n      assert(client.isConnected);\n\n      assertEquals(await client.ping(), \"PONG\");\n\n      client.close();\n    });\n\n    it(\"fires events\", async () => {\n      const client = await connect(getOpts());\n\n      let closeEventFired = false,\n        endEventFired = false;\n\n      const firedEvents: Array<string> = [];\n      client.addEventListener(\"close\", () => {\n        closeEventFired = true;\n        firedEvents.push(\"close\");\n      });\n      client.addEventListener(\"end\", () => {\n        endEventFired = true;\n        firedEvents.push(\"end\");\n      });\n      // @ts-expect-error unkwnon events should be denied\n      client.addEventListener(\"no-such-event\", () => {\n        firedEvents.push(\"no-such-event\");\n      });\n\n      client.close();\n\n      assertEquals(closeEventFired, true);\n      assertEquals(endEventFired, true);\n      assertEquals(firedEvents, [\"close\", \"end\"]);\n    });\n\n    it(\"fires events with a lazy client\", async () => {\n      const client = createLazyClient(getOpts());\n      const firedEvents: Array<string> = [];\n\n      client.addEventListener(\"connect\", (e) => {\n        firedEvents.push(\"connect\");\n        assertInstanceOf(e, CustomEvent);\n      });\n      client.addEventListener(\"ready\", (e) => {\n        firedEvents.push(\"ready\");\n        assertInstanceOf(e, CustomEvent);\n      }, { once: true });\n\n      client.addEventListener(\"close\", (e) => {\n        firedEvents.push(\"close\");\n        assertInstanceOf(e, CustomEvent);\n      });\n      client.addEventListener(\"end\", (e) => {\n        firedEvents.push(\"end\");\n        assertInstanceOf(e, CustomEvent);\n      });\n\n      await client.exists(\"foo\");\n      assertEquals(firedEvents, [\"connect\", \"ready\"]);\n      client.close();\n      assertEquals(firedEvents, [\"connect\", \"ready\", \"close\", \"end\"]);\n\n      await client.connect();\n      await client.exists(\"foo\");\n      client.close();\n\n      assertEquals(firedEvents, [\n        \"connect\",\n        \"ready\",\n        \"close\",\n        \"end\",\n        \"connect\",\n        \"close\",\n        \"end\",\n      ]);\n    });\n  });\n\n  describe(\"using\", () => {\n    it(\"implements `Symbol.dispose`\", async () => {\n      using client = await connect(getOpts());\n      assert(client.isConnected);\n      assert(!client.isClosed);\n    });\n  });\n}\n\nfunction parseCommandStats(\n  stats: string,\n): Record<string, Record<string, number>> {\n  return stats.split(\"\\r\\n\").reduce((statsByCommand, line) => {\n    if (line.startsWith(\"#\") || line.length === 0) {\n      return statsByCommand;\n    }\n\n    const [section, details] = line.split(\":\");\n    assertExists(section);\n    assertExists(details);\n    const sectionPrefix = \"cmdstat_\";\n    assert(section.startsWith(sectionPrefix));\n    const command = section.slice(sectionPrefix.length);\n    statsByCommand[command] = details.split(\",\").reduce((stats, attr) => {\n      const [key, value] = attr.split(\"=\");\n      assertExists(key);\n      assertExists(value);\n      stats[key] = parseInt(value);\n      return stats;\n    }, {} as Record<string, number>);\n\n    return statsByCommand;\n  }, {} as Record<string, Record<string, number>>);\n}\n"
  },
  {
    "path": "tests/commands/general.ts",
    "content": "import { ErrorReplyError } from \"../../mod.ts\";\nimport type { Redis } from \"../../mod.ts\";\nimport { assertEquals, assertRejects } from \"../../deps/std/assert.ts\";\nimport {\n  afterAll,\n  beforeAll,\n  beforeEach,\n  describe,\n  it,\n} from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\n\nexport function generalTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  const getOpts = () => ({\n    hostname: \"127.0.0.1\",\n    port: getServer().port,\n  });\n  let client!: Redis;\n  beforeAll(async () => {\n    client = await connect(getOpts());\n  });\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  afterAll(() => client.close());\n\n  it(\"can send multiple commands conccurently\", async () => {\n    let promises: Promise<string | null>[] = [];\n    for (const key of [\"a\", \"b\", \"c\"]) {\n      promises.push(client.set(key, key));\n    }\n    await Promise.all(promises);\n    promises = [];\n    for (const key of [\"a\", \"b\", \"c\"]) {\n      promises.push(client.get(key));\n    }\n    const [a, b, c] = await Promise.all(promises);\n    assertEquals(a, \"a\");\n    assertEquals(b, \"b\");\n    assertEquals(c, \"c\");\n  });\n\n  it(\"can treat `null` and `undefined`\", async () => {\n    // @ts-expect-error This error is intended\n    await client.set(\"null\", null);\n\n    // @ts-expect-error This error is intended\n    await client.set(\"undefined\", undefined);\n\n    assertEquals(await client.get(\"null\"), \"\");\n    assertEquals(await client.get(\"undefined\"), \"\");\n  });\n\n  describe(\"connect\", () => {\n    it(\"selects the DB specified by `opts.db`\", async () => {\n      const opts = getOpts();\n      const key = \"exists\";\n      const client1 = await connect({ ...opts, db: 0 });\n      try {\n        await client1.set(key, \"aaa\");\n        const exists = await client1.exists(key);\n        assertEquals(exists, 1);\n      } finally {\n        client1.close();\n      }\n\n      const client2 = await connect({ ...opts, db: 0 });\n      try {\n        const exists = await client2.exists(key);\n        assertEquals(exists, 1);\n      } finally {\n        client2.close();\n      }\n\n      const client3 = await connect({ ...opts, db: 1 });\n      try {\n        const exists = await client3.exists(key);\n        assertEquals(exists, 0);\n      } finally {\n        client3.close();\n      }\n    });\n\n    it(\"throws an error if a wrong password is given\", async () => {\n      const { port } = getOpts();\n      await assertRejects(async () => {\n        await connect({\n          hostname: \"127.0.0.1\",\n          port,\n          password: \"wrong_password\",\n        });\n      }, ErrorReplyError);\n    });\n\n    it(\"throws an error if an empty password is given\", async () => {\n      const { port } = getOpts();\n      // In Redis, authentication with an empty password will always fail.\n      await assertRejects(async () => {\n        await connect({\n          hostname: \"127.0.0.1\",\n          port,\n          password: \"\",\n        });\n      }, ErrorReplyError);\n    });\n  });\n\n  describe(\"exists\", () => {\n    it(\"returns if `key` exists\", async () => {\n      const opts = getOpts();\n      const key = \"exists\";\n      const client1 = await connect({ ...opts, db: 0 });\n      await client1.set(key, \"aaa\");\n      const exists1 = await client1.exists(key);\n      assertEquals(exists1, 1);\n      const client2 = await connect({ ...opts, db: 1 });\n      const exists2 = await client2.exists(key);\n      assertEquals(exists2, 0);\n      client1.close();\n      client2.close();\n    });\n\n    it(\"can handle many keys\", async () => {\n      const reply = await client.exists(\n        \"a\",\n        \"b\",\n        \"c\",\n        \"d\",\n        \"e\",\n        \"f\",\n        \"g\",\n        \"h\",\n        \"i\",\n        \"j\",\n      );\n      assertEquals(reply, 0);\n    });\n  });\n\n  describe(\"invalid port\", () => {\n    for (const v of [Infinity, NaN, \"\", \"port\"]) {\n      it(`throws an error if \\`${v}\\` is given`, async () => {\n        await assertRejects(\n          async () => {\n            await connect({\n              hostname: \"127.0.0.1\",\n              port: v,\n              maxRetryCount: 0,\n            });\n          },\n          Error,\n          \"invalid\",\n        );\n      });\n    }\n  });\n\n  describe(\"sendCommand\", () => {\n    it(\"can handle simple types\", async () => {\n      // simple string\n      {\n        const reply = await client.sendCommand(\"SET\", [\"key\", \"a\"]);\n        assertEquals(reply, \"OK\");\n      }\n\n      // bulk string\n      {\n        const reply = await client.sendCommand(\"GET\", [\"key\"]);\n        assertEquals(reply, \"a\");\n      }\n\n      // integer\n      {\n        const reply = await client.sendCommand(\"EXISTS\", [\"key\"]);\n        assertEquals(reply, 1);\n      }\n    });\n\n    it(\"returns simple strings or blob strings as `Uint8Array` if `returnUint8Arrays` is set to `true`\", async () => {\n      const encoder = new TextEncoder();\n\n      await client.set(\"key\", encoder.encode(\"hello\"));\n      const reply = await client.sendCommand(\"GET\", [\"key\"], {\n        returnUint8Arrays: true,\n      });\n      assertEquals(reply, encoder.encode(\"hello\"));\n    });\n  });\n\n  describe(\"automatic reconnection\", () => {\n    it(\"reconnects when the connection is lost\", async () => {\n      const tempClient = await connect(getOpts());\n      try {\n        const id = await tempClient.clientID();\n        await client.clientKill({ id });\n        const reply = await tempClient.ping();\n        assertEquals(reply, \"PONG\");\n      } finally {\n        tempClient.close();\n      }\n    });\n\n    it(\"fails when max retry count is exceeded\", async () => {\n      const tempClient = await connect({\n        ...getOpts(),\n        maxRetryCount: 0,\n      });\n      try {\n        const id = await tempClient.clientID();\n        await client.clientKill({ id });\n        await assertRejects(() => tempClient.ping());\n      } finally {\n        tempClient.close();\n      }\n    });\n\n    it(\"does not reconnect when the connection is manually closed by the user\", async () => {\n      const tempClient = await connect(getOpts());\n      tempClient.close();\n      await assertRejects(() => tempClient.ping());\n    });\n  });\n}\n"
  },
  {
    "path": "tests/commands/geo.ts",
    "content": "import { assertEquals } from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport { usesRedisVersion } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function geoTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    client = await connect({ hostname: \"127.0.0.1\", port: getServer().port });\n  });\n\n  afterAll(() => client.close());\n\n  it(\"geoadd\", async () => {\n    assertEquals(\n      await client.geoadd(\"Sicily\", 13.361389, 38.115556, \"Palermo\"),\n      1,\n    );\n    assertEquals(\n      await client.geoadd(\"Sicily\", 15.087269, 37.502669, \"Catania\"),\n      1,\n    );\n    assertEquals(\n      await client.geoadd(\"Sicily\", {\n        Palermo: [13.361389, 38.115556],\n        Catania: [15.087269, 37.502669],\n      }),\n      0,\n    );\n    assertEquals(\n      await client.geoadd(\n        \"Sicily\",\n        [13.361389, 38.115556, \"Palermo\"],\n        [15.087269, 37.502669, \"Catania\"],\n      ),\n      0,\n    );\n  });\n\n  it(\"geohash\", async () => {\n    await client.geoadd(\"Sicily\", {\n      Palermo: [13.361389, 38.115556],\n      Catania: [15.087269, 37.502669],\n    });\n    const resp = await client.geohash(\"Sicily\", \"Palermo\", \"Catania\", \"Enna\");\n    assertEquals(resp, [\"sqc8b49rny0\", \"sqdtr74hyu0\", null]);\n  });\n\n  it(\"geopos\", async () => {\n    await client.geoadd(\"Sicily\", {\n      Palermo: [13.361389, 38.115556],\n      Catania: [15.087269, 37.502669],\n    });\n    const resp = await client.geopos(\"Sicily\", \"Palermo\", \"Catania\", \"Enna\");\n    const usesRedis8 = usesRedisVersion(\"8\");\n    assertEquals(resp, [\n      usesRedis8\n        ? [\"13.361389338970184\", \"38.1155563954963\"]\n        : [\"13.36138933897018433\", \"38.11555639549629859\"],\n      usesRedis8\n        ? [\"15.087267458438873\", \"37.50266842333162\"]\n        : [\"15.08726745843887329\", \"37.50266842333162032\"],\n      null,\n    ]);\n  });\n\n  it(\"geodist\", async () => {\n    await client.geoadd(\"Sicily\", {\n      Palermo: [13.361389, 38.115556],\n      Catania: [15.087269, 37.502669],\n    });\n    let resp = await client.geodist(\"Sicily\", \"Palermo\", \"Catania\");\n    assertEquals(resp, \"166274.1516\");\n    resp = await client.geodist(\"Sicily\", \"Palermo\", \"Enna\");\n    assertEquals(resp, null);\n  });\n\n  it(\"georadius\", async () => {\n    await client.georadius(\"Test\", 0, 1, 10, \"km\");\n  });\n\n  it(\"georadiusbymember\", async () => {\n    await client.georadiusbymember(\"Sicily\", \"Palermo\", 10, \"km\");\n  });\n}\n"
  },
  {
    "path": "tests/commands/hash.ts",
    "content": "import { assertEquals } from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, beforeEach, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function hashTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"hdel\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hdel(\"key\", \"f1\", \"f2\", \"f3\"), 2);\n  });\n\n  it(\"hexists\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    assertEquals(await client.hexists(\"key\", \"f1\"), 1);\n    assertEquals(await client.hexists(\"key\", \"f2\"), 0);\n  });\n\n  it(\"hget\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    assertEquals(await client.hget(\"key\", \"f1\"), \"1\");\n    assertEquals(await client.hget(\"key\", \"f2\"), null);\n  });\n\n  it(\"hgetall\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hgetall(\"key\"), [\"f1\", \"1\", \"f2\", \"2\"]);\n  });\n\n  it(\"hincrby\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    assertEquals(await client.hincrby(\"key\", \"f1\", 4), 5);\n  });\n\n  it(\"hincybyfloat\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    assertEquals(await client.hincrbyfloat(\"key\", \"f1\", 4.33), \"5.33\");\n  });\n\n  it(\"hkeys\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hkeys(\"key\"), [\"f1\", \"f2\"]);\n  });\n\n  it(\"hlen\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hlen(\"key\"), 2);\n  });\n\n  it(\"hmget\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hmget(\"key\", \"f1\", \"f2\", \"f3\"), [\n      \"1\",\n      \"2\",\n      null,\n    ]);\n  });\n\n  it(\"hmset\", async () => {\n    assertEquals(await client.hmset(\"key\", \"f1\", \"1\"), \"OK\");\n    assertEquals(await client.hmset(\"key\", { f1: \"1\", f2: \"2\" }), \"OK\");\n    assertEquals(await client.hmset(\"key\", [\"f4\", \"4\"], [\"f5\", \"5\"]), \"OK\");\n  });\n\n  it(\"hset\", async () => {\n    assertEquals(await client.hset(\"key\", \"f1\", \"1\"), 1);\n    assertEquals(await client.hset(\"key\", { f2: \"2\", f3: \"3\" }), 2);\n    assertEquals(await client.hset(\"key\", [\"f4\", \"4\"], [\"f5\", \"5\"]), 2);\n  });\n\n  it(\"hsetnx\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    assertEquals(await client.hsetnx(\"key\", \"f1\", \"1\"), 0);\n    assertEquals(await client.hsetnx(\"key\", \"f2\", \"2\"), 1);\n  });\n\n  it(\"hstrlen\", async () => {\n    await client.hset(\"key\", \"f1\", \"abc\");\n    assertEquals(await client.hstrlen(\"key\", \"f1\"), 3);\n  });\n\n  it(\"hvals\", async () => {\n    await client.hset(\"key\", \"f1\", \"1\");\n    await client.hset(\"key\", \"f2\", \"2\");\n    assertEquals(await client.hvals(\"key\"), [\"1\", \"2\"]);\n  });\n\n  it(\"hscan\", async () => {\n    assertEquals(Array.isArray(await client.hscan(\"key\", 0)), true);\n  });\n}\n"
  },
  {
    "path": "tests/commands/hyper_loglog.ts",
    "content": "import { assertEquals } from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function hyperloglogTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n  afterAll(() => client.close());\n\n  it(\"pdfadd\", async () => {\n    assertEquals(await client.pfadd(\"hll\", \"a\", \"b\", \"c\", \"d\"), 1);\n  });\n\n  it(\"pdfcount\", async () => {\n    await client.pfadd(\"hll\", \"a\", \"b\", \"c\", \"d\");\n    assertEquals(await client.pfcount(\"hll\"), 4);\n  });\n\n  it(\"pfmerge\", async () => {\n    await client.pfadd(\"hll\", \"a\", \"b\", \"c\", \"d\");\n    await client.pfadd(\"hll2\", \"1\", \"2\", \"3\", \"4\");\n    assertEquals(await client.pfmerge(\"hll\", \"hll2\"), \"OK\");\n  });\n}\n"
  },
  {
    "path": "tests/commands/key.ts",
    "content": "import {\n  assert,\n  assertArrayIncludes,\n  assertEquals,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, beforeEach, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function keyTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const { port } = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port });\n  });\n  afterAll(() => client.close());\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"del\", async () => {\n    let s = await client.set(\"key1\", \"fuga\");\n    assertEquals(s, \"OK\");\n    s = await client.set(\"key2\", \"fugaaa\");\n    assertEquals(s, \"OK\");\n    const deleted = await client.del(\"key1\", \"key2\");\n    assertEquals(deleted, 2);\n  });\n\n  it(\"dump and restore\", async () => {\n    await client.set(\"key\", \"hello\");\n    const v = await client.dump(\"key\");\n    await client.del(\"key\");\n    await client.restore(\"key\", 2000, v!);\n    assertEquals(await client.get(\"key\"), \"hello\");\n  });\n\n  it(\"exists\", async () => {\n    const none = await client.exists(\"none\", \"none2\");\n    assertEquals(none, 0);\n    await client.set(\"exists\", \"aaa\");\n    const exists = await client.exists(\"exists\", \"none\");\n    assertEquals(exists, 1);\n  });\n\n  it(\"expire\", async () => {\n    await client.set(\"key\", \"foo\");\n    const v = await client.expire(\"key\", 1);\n    assertEquals(v, 1);\n  });\n\n  it(\"expireat\", async () => {\n    await client.set(\"key\", \"bar\");\n    const timestamp = String(new Date(8640000000000000).getTime() / 1000);\n    const v = await client.expireat(\"key\", timestamp);\n    assertEquals(v, 1);\n  });\n\n  it(\"keys\", async () => {\n    await client.set(\"key1\", \"foo\");\n    await client.set(\"key2\", \"bar\");\n    const v = await client.keys(\"key*\");\n    assertEquals(v.sort(), [\"key1\", \"key2\"]);\n  });\n\n  it(\"migrate\", async () => {\n    const { port } = getServer();\n    const v = await client.migrate(\"127.0.0.1\", port, \"nosuchkey\", \"0\", 0);\n    assertEquals(v, \"NOKEY\");\n  });\n\n  it(\"move\", async () => {\n    const v = await client.move(\"nosuchkey\", \"1\");\n    assertEquals(v, 0);\n  });\n\n  it(\"object refcount\", async () => {\n    await client.set(\"key\", \"hello\");\n    const v = await client.objectRefCount(\"key\");\n    assertEquals(v, 1);\n  });\n\n  it(\"object encoding\", async () => {\n    await client.set(\"key\", \"foobar\");\n    const v = await client.objectEncoding(\"key\");\n    assertEquals(typeof v, \"string\");\n  });\n\n  it(\"object idletime\", async () => {\n    await client.set(\"key\", \"baz\");\n    const v = await client.objectIdletime(\"key\");\n    assertEquals(v, 0);\n  });\n\n  it(\"object freq\", async () => {\n    const v = await client.objectFreq(\"nosuchkey\");\n    assertEquals(v, null);\n  });\n\n  it(\"object help\", async () => {\n    const v = await client.objectHelp();\n    assert(Array.isArray(v));\n  });\n\n  it(\"persist\", async () => {\n    const v = await client.persist(\"nosuckey\");\n    assertEquals(v, 0);\n  });\n\n  it(\"pexpire\", async () => {\n    await client.set(\"key\", \"hello\");\n    const v = await client.pexpire(\"key\", 500);\n    assertEquals(v, 1);\n  });\n\n  it(\"pexpireat\", async () => {\n    await client.set(\"key\", \"bar\");\n    const timestamp = new Date(8640000000000000).getTime();\n    const v = await client.pexpireat(\"key\", timestamp);\n    assertEquals(v, 1);\n  });\n\n  it(\"pttl\", async () => {\n    await client.set(\"key\", \"foo\");\n    const v = await client.pttl(\"key\");\n    assertEquals(v, -1);\n  });\n\n  it(\"randomkey\", async () => {\n    await client.set(\"key\", \"hello\");\n    const v = await client.randomkey();\n    assertEquals(typeof v, \"string\");\n  });\n\n  it(\"rename\", async () => {\n    await client.set(\"key\", \"foo\");\n    const v = await client.rename(\"key\", \"newkey\");\n    assertEquals(v, \"OK\");\n  });\n\n  it(\"renamenx\", async () => {\n    await client.set(\"key\", \"bar\");\n    const v = await client.renamenx(\"key\", \"newkey\");\n    assertEquals(v, 1);\n  });\n\n  it(\"sort\", async () => {\n    await client.rpush(\"key\", \"3\", \"10\", \"5\", \"1\");\n    const v = await client.sort(\"key\");\n    assertEquals(v, [\"1\", \"3\", \"5\", \"10\"]);\n  });\n\n  it(\"sort with multiple patterns\", async () => {\n    // https://github.com/denodrivers/redis/pull/364\n    await client.rpush(\"ids\", \"1\", \"2\", \"3\");\n    await client.mset({\n      \"weight_1\": \"8\",\n      \"weight_2\": \"2\",\n      \"weight_3\": \"5\",\n      \"name_1\": \"foo\",\n      \"name_2\": \"bar\",\n      \"name_3\": \"baz\",\n    });\n    const result = await client.sort(\"ids\", {\n      by: \"weight_*\",\n      patterns: [\"#\", \"name_*\"],\n    });\n    assertEquals(result, [\n      \"2\",\n      \"bar\",\n      \"3\",\n      \"baz\",\n      \"1\",\n      \"foo\",\n    ]);\n  });\n\n  it(\"touch\", async () => {\n    await client.set(\"key1\", \"baz\");\n    await client.set(\"key2\", \"qux\");\n    const v = await client.touch(\"key1\", \"key2\");\n    assertEquals(v, 2);\n  });\n\n  it(\"ttl\", async () => {\n    await client.set(\"key\", \"foo\");\n    const v = await client.ttl(\"key\");\n    assertEquals(v, -1);\n  });\n\n  it(\"type\", async () => {\n    await client.set(\"key\", \"foobar\");\n    const v = await client.type(\"key\");\n    assertEquals(v, \"string\");\n  });\n\n  it(\"unlink\", async () => {\n    await client.set(\"key1\", \"hello\");\n    await client.set(\"key2\", \"world\");\n    const v = await client.unlink(\"key1\", \"key2\", \"nosuchkey\");\n    assertEquals(v, 2);\n  });\n\n  it(\"wait\", async () => {\n    await client.set(\"key\", \"hello\");\n    const v = await client.wait(0, 1000);\n    assertEquals(v, 0);\n  });\n\n  it(\"scan\", async () => {\n    await client.set(\"key1\", \"foo\");\n    await client.set(\"key2\", \"bar\");\n    const v = await client.scan(0);\n    assertEquals(v.length, 2);\n    assertEquals(v[0], \"0\");\n    assertEquals(v[1].length, 2);\n    assertArrayIncludes(v[1], [\"key1\", \"key2\"]);\n  });\n\n  it(\"scan with pattern\", async () => {\n    await client.set(\"foo\", \"f\");\n    await client.set(\"bar\", \"b\");\n    const v = await client.scan(0, { pattern: \"f*\" });\n    assertEquals(v, [\"0\", [\"foo\"]]);\n  });\n}\n"
  },
  {
    "path": "tests/commands/latency.ts",
    "content": "import { assertStringIncludes } from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, describe, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function latencyTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  const getOpts = () => ({\n    hostname: \"127.0.0.1\",\n    port: getServer().port,\n  });\n  beforeAll(async () => {\n    client = await connect(getOpts());\n  });\n  afterAll(() => client.close());\n\n  describe(\"latencyDoctor\", () => {\n    it(\"executes `LATENCY DOCTOR`\", async () => {\n      const report = await client.latencyDoctor();\n      assertStringIncludes(\n        report,\n        \"Latency monitoring is disabled in this Redis instance.\",\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "tests/commands/list.ts",
    "content": "import { assertEquals } from \"../../deps/std/assert.ts\";\nimport {\n  afterAll,\n  beforeAll,\n  beforeEach,\n  describe,\n  it,\n} from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function listTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"blpop\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.blpop(2, \"list\"), [\"list\", \"1\"]);\n  });\n\n  it(\"blpop returns null on timeout\", async () => {\n    assertEquals(await client.blpop(1, \"list\"), null);\n  });\n\n  it(\"brpop\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.brpop(2, \"list\"), [\"list\", \"2\"]);\n  });\n\n  it(\"brpop returns null on timeout\", async () => {\n    assertEquals(await client.brpop(1, \"list\"), null);\n  });\n\n  it(\"brpoplpush\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.brpoplpush(\"list\", \"list\", 2), \"2\");\n  });\n\n  it(\"brpoplpush returns null on timeout\", async () => {\n    assertEquals(await client.brpoplpush(\"list\", \"list\", 1), null);\n  });\n\n  it(\"lindex\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lindex(\"list\", 0), \"1\");\n    assertEquals(await client.lindex(\"list\", 3), null);\n  });\n\n  it(\"linsert\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.linsert(\"list\", \"BEFORE\", \"2\", \"1.5\"), 3);\n  });\n\n  it(\"llen\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.llen(\"list\"), 2);\n  });\n\n  describe(\"lpop\", () => {\n    it(\"pops the first element of the list\", async () => {\n      await client.rpush(\"list\", \"1\", \"2\");\n      assertEquals(await client.lpop(\"list\"), \"1\");\n    });\n\n    it(\"supports the `count` argument\", async () => {\n      await client.rpush(\"list\", \"foo\", \"bar\", \"baz\");\n      assertEquals(await client.lpop(\"list\", 2), [\"foo\", \"bar\"]);\n    });\n  });\n\n  it(\"lpos\", async () => {\n    await client.rpush(\"list\", \"a\", \"b\", \"c\", \"1\");\n    assertEquals(await client.lpos(\"list\", \"c\"), 2);\n    assertEquals(await client.lpos(\"list\", \"d\"), null);\n  });\n\n  it(\"lpos with rank\", async () => {\n    await client.rpush(\"list\", \"a\", \"b\", \"c\", \"1\", \"2\", \"c\", \"c\", \"d\");\n    assertEquals(await client.lpos(\"list\", \"c\", { rank: 2 }), 5);\n  });\n\n  it(\"lpos with count\", async () => {\n    await client.rpush(\"list\", \"a\", \"b\", \"c\", \"1\", \"2\", \"b\", \"c\");\n    assertEquals(await client.lpos(\"list\", \"b\", { count: 2 }), [1, 5]);\n  });\n\n  it(\"lpos with maxlen\", async () => {\n    await client.rpush(\"list\", \"a\", \"b\", \"c\");\n    assertEquals(await client.lpos(\"list\", \"c\", { maxlen: 2 }), null);\n    assertEquals(await client.lpos(\"list\", \"c\", { maxlen: 3 }), 2);\n  });\n\n  it(\"lpush\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lpush(\"list\", \"3\", \"4\"), 4);\n  });\n\n  it(\"lpushx\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lpushx(\"list\", \"3\"), 3);\n  });\n\n  it(\"lrange\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lrange(\"list\", 0, -1), [\"1\", \"2\"]);\n  });\n\n  it(\"lrem\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lrem(\"list\", 0, \"1\"), 1);\n  });\n\n  it(\"lset\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.lset(\"list\", 0, \"0\"), \"OK\");\n  });\n\n  it(\"ltrim\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.ltrim(\"list\", 0, 1), \"OK\");\n  });\n\n  it(\"rpop\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.rpop(\"list\"), \"2\");\n  });\n\n  it(\"rpoplpush\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.rpoplpush(\"list\", \"list\"), \"2\");\n  });\n\n  it(\"rpush\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.rpush(\"list\", \"3\"), 3);\n  });\n\n  it(\"rpoplpush\", async () => {\n    await client.rpush(\"list\", \"1\", \"2\");\n    assertEquals(await client.rpushx(\"list\", \"3\"), 3);\n  });\n}\n"
  },
  {
    "path": "tests/commands/pipeline.ts",
    "content": "import type { Raw } from \"../../mod.ts\";\nimport { ErrorReplyError } from \"../../mod.ts\";\nimport { assert, assertEquals } from \"../../deps/std/assert.ts\";\nimport { it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\n\nexport function pipelineTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  const getOpts = () => ({\n    hostname: \"127.0.0.1\",\n    port: getServer().port,\n  });\n\n  it(\"testPipeline\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const pl = client.pipeline();\n    await Promise.all([\n      pl.ping(),\n      pl.ping(),\n      pl.set(\"set1\", \"value1\"),\n      pl.set(\"set2\", \"value2\"),\n      pl.mget(\"set1\", \"set2\"),\n      pl.del(\"set1\"),\n      pl.del(\"set2\"),\n    ]);\n    const ret = await pl.flush();\n    assertEquals(ret, [\n      \"PONG\",\n      \"PONG\",\n      \"OK\",\n      \"OK\",\n      [\"value1\", \"value2\"],\n      1,\n      1,\n    ]);\n    client.close();\n  });\n\n  it(\"testTx\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const tx1 = client.tx();\n    const tx2 = client.tx();\n    const tx3 = client.tx();\n    await client.del(\"key\");\n    await Promise.all<unknown>([\n      tx1.get(\"key\"),\n      tx1.incr(\"key\"),\n      tx1.incr(\"key\"),\n      tx1.incr(\"key\"),\n      tx1.get(\"key\"),\n      //\n      tx2.get(\"key\"),\n      tx2.incr(\"key\"),\n      tx2.incr(\"key\"),\n      tx2.incr(\"key\"),\n      tx2.get(\"key\"),\n      //\n      tx3.get(\"key\"),\n      tx3.incr(\"key\"),\n      tx3.incr(\"key\"),\n      tx3.incr(\"key\"),\n      tx3.get(\"key\"),\n    ]);\n    const rep1 = await tx1.flush() as Array<Raw>;\n    const rep2 = await tx2.flush() as Array<Raw>;\n    const rep3 = await tx3.flush() as Array<Raw>;\n    assertEquals(\n      parseInt(rep1[4] as string),\n      parseInt(rep1[0] as string) + 3,\n    );\n    assertEquals(\n      parseInt(rep2[4] as string),\n      parseInt(rep2[0] as string) + 3,\n    );\n    assertEquals(\n      parseInt(rep3[4] as string),\n      parseInt(rep3[0] as string) + 3,\n    );\n    client.close();\n  });\n\n  it(\"pipeline in concurrent\", async () => {\n    {\n      const opts = getOpts();\n      const client = await connect(opts);\n      const tx = client.pipeline();\n      const promises: Promise<unknown>[] = [];\n      await client.del(\"a\", \"b\", \"c\");\n      for (const key of [\"a\", \"b\", \"c\"]) {\n        promises.push(tx.set(key, key));\n      }\n      promises.push(tx.flush());\n      for (const key of [\"a\", \"b\", \"c\"]) {\n        promises.push(tx.get(key));\n      }\n      promises.push(tx.flush());\n      const res = await Promise.all(promises);\n\n      assertEquals(res, [\n        \"OK\", // set(a)\n        \"OK\", // set(b)\n        \"OK\", // set(c)\n        [\"OK\", \"OK\", \"OK\"], // flush()\n        \"OK\", // get(a)\n        \"OK\", // get(b)\n        \"OK\", // get(c)\n        [\"a\", \"b\", \"c\"], // flush()\n      ]);\n\n      client.close();\n    }\n  });\n\n  it(\"pipeline in concurrent, avoid redundant response mixup\", async () => {\n    {\n      const opts = getOpts();\n      const client = await connect(opts);\n\n      const randomValues = new Array(10)\n        .fill(0)\n        .map(() => new Array(10).fill(0).map(() => Math.random().toString()));\n\n      for (let i = 0; i < 10; i++) {\n        const key = `list_${i}`;\n        const values = randomValues[i];\n        await client.del(key);\n        await client.rpush(key, ...values);\n      }\n\n      const task = async () => {\n        const tx = client.pipeline();\n        for (let i = 0; i < 10; i++) {\n          tx.lrange(`list_${i}`, 0, -1);\n        }\n        return await tx.flush();\n      };\n\n      const res = await Promise.all([task(), task()]);\n      assertEquals(res, [randomValues, randomValues]);\n      client.close();\n    }\n  });\n\n  it(\"error while pipeline\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const tx = client.pipeline();\n    tx.set(\"a\", \"a\");\n    tx.eval(\"var\", [\"k\"], [\"v\"]);\n    tx.get(\"a\");\n    const resp = await tx.flush() as Array<Raw>;\n    assertEquals(resp.length, 3);\n    assertEquals(resp[0], \"OK\");\n    assert(resp[1] instanceof ErrorReplyError);\n    assertEquals(resp[2], \"a\");\n    client.close();\n  });\n}\n"
  },
  {
    "path": "tests/commands/pubsub.ts",
    "content": "import { delay } from \"../../deps/std/async.ts\";\nimport { assert, assertEquals, assertRejects } from \"../../deps/std/assert.ts\";\nimport { describe, it } from \"../../deps/std/testing.ts\";\nimport { nextPort, startRedis, stopRedis } from \"../test_util.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\n\nexport function pubsubTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  const getOpts = () => ({ hostname: \"127.0.0.1\", port: getServer().port });\n\n  it(\"supports unsubscribing channels by `unsubscribe()`\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const sub = await client.subscribe(\"subsc\");\n    await sub.unsubscribe(\"subsc\");\n    sub.close();\n    assertEquals(sub.isClosed, true);\n    client.close();\n  });\n\n  it(\"supports reading messages sequentially by `receive()`\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const pub = await connect(opts);\n    const sub = await client.subscribe(\"subsc2\");\n    const p = (async function () {\n      const it = sub.receive();\n      return (await it.next()).value;\n    })();\n    await pub.publish(\"subsc2\", \"wayway\");\n    const message = await p;\n    assertEquals(message, {\n      channel: \"subsc2\",\n      message: \"wayway\",\n    });\n    sub.close();\n    assertEquals(sub.isClosed, true);\n    assertEquals(client.isClosed, true);\n    pub.close();\n    await assertRejects(async () => {\n      await client.get(\"aaa\");\n    }, Deno.errors.BadResource);\n  });\n\n  describe(\"receiveBuffers\", () => {\n    it(\"returns messages as Uint8Array\", async () => {\n      const opts = getOpts();\n      const client = await connect(opts);\n      const pub = await connect(opts);\n      const sub = await client.subscribe(\"subsc3\");\n      const p = (async () => {\n        const it = sub.receiveBuffers();\n        return (await it.next()).value;\n      })();\n      try {\n        await pub.publish(\"subsc3\", \"foobar\");\n        const message = await p;\n        assertEquals(message, {\n          channel: \"subsc3\",\n          message: new TextEncoder().encode(\"foobar\"),\n        });\n      } finally {\n        sub.close();\n        pub.close();\n      }\n      assert(sub.isClosed);\n      assert(client.isClosed);\n    });\n  });\n\n  it(\"supports `psubscribe()`\", async () => {\n    const opts = getOpts();\n    const client = await connect(opts);\n    const pub = await connect(opts);\n    const sub = await client.psubscribe(\"ps*\");\n    let message1;\n    let message2;\n    const it = sub.receive();\n    const p = (async function () {\n      message1 = (await it.next()).value;\n      message2 = (await it.next()).value;\n    })();\n    await pub.publish(\"psub\", \"wayway\");\n    await pub.publish(\"psubs\", \"heyhey\");\n    await p;\n    assertEquals(message1, {\n      pattern: \"ps*\",\n      channel: \"psub\",\n      message: \"wayway\",\n    });\n    assertEquals(message2, {\n      pattern: \"ps*\",\n      channel: \"psubs\",\n      message: \"heyhey\",\n    });\n    sub.close();\n    pub.close();\n    client.close();\n  });\n\n  it(\"supports automatic reconnection of subscribers\", async () => {\n    const opts = getOpts();\n    const port = nextPort();\n    let tempServer = await startRedis({ port });\n    const subscriberClient = await connect({ ...opts, port });\n    const backoff = () => 1200;\n    const publisher = await connect({\n      ...opts,\n      backoff,\n      maxRetryCount: 10,\n      port,\n    });\n    const subscription = await subscriberClient.psubscribe(\"ps*\");\n    const it = subscription.receive();\n\n    let messages = 0;\n\n    const interval = setInterval(async () => {\n      await publisher.publish(\"psub\", \"wayway\");\n      messages++;\n    }, 900);\n\n    // Intentionally stops the server after the first message is delivered.\n    setTimeout(() => stopRedis(tempServer), 1000);\n\n    const { promise, resolve, reject } = Promise.withResolvers<void>();\n    setTimeout(async () => {\n      try {\n        // At this point, the server is assumed to be stopped.\n        // The subscriber and publisher should attempt to reconnect.\n        assertEquals(\n          subscriberClient.isConnected,\n          false,\n          \"The subscriber client still thinks it is connected.\",\n        );\n        assertEquals(\n          publisher.isConnected,\n          false,\n          \"The publisher client still thinks it is connected.\",\n        );\n        assert(messages >= 1, \"At least one message should be published.\");\n        assert(messages < 5, \"Too many messages were published.\");\n\n        // Reboot the server.\n        tempServer = await startRedis({ port });\n\n        const tempClient = await connect({ ...opts, port });\n        await tempClient.ping();\n        tempClient.close();\n\n        // Wait for the subscriber and publisher to reconnect...\n        await delay(1000);\n\n        assert(\n          subscriberClient.isConnected,\n          \"The subscriber client is not connected.\",\n        );\n        assert(publisher.isConnected, \"The publisher client is not connected.\");\n\n        resolve();\n      } catch (error) {\n        reject(error);\n      }\n    }, 2000);\n\n    // Block until all resolve\n    await Promise.all([it.next(), it.next(), it.next(), it.next(), it.next()]);\n\n    // Cleanup\n    clearInterval(interval);\n    subscription.close();\n    publisher.close();\n    subscriberClient.close();\n    await stopRedis(tempServer);\n    await promise;\n  });\n\n  it({\n    ignore: true,\n    name:\n      \"SubscriptionShouldNotThrowBadResourceErrorWhenConnectionIsClosed (#89)\",\n    fn: async () => {\n      const opts = getOpts();\n      const redis = await connect(opts);\n      const sub = await redis.subscribe(\"test\");\n      const subscriptionPromise = (async () => {\n        // deno-lint-ignore no-empty -- Verifying that no exceptions are thrown.\n        for await (const _ of sub.receive()) {}\n      })();\n      redis.close();\n      await subscriptionPromise;\n      assert(sub.isClosed);\n    },\n  });\n\n  it(\"supports `pubsubNumsub()`\", async () => {\n    const opts = getOpts();\n    const subClient1 = await connect(opts);\n    await subClient1.subscribe(\"test1\", \"test2\");\n\n    const subClient2 = await connect(opts);\n    await subClient2.subscribe(\"test2\", \"test3\");\n\n    const pubClient = await connect(opts);\n    const resp = await pubClient.pubsubNumsub(\"test1\", \"test2\", \"test3\");\n    assertEquals(resp, [\"test1\", 1, \"test2\", 2, \"test3\", 1]);\n\n    subClient1.close();\n    subClient2.close();\n    pubClient.close();\n  });\n\n  it(\"supports calling `subscribe()` multiple times\", async () => {\n    // https://github.com/denodrivers/redis/issues/390\n    const opts = getOpts();\n    const redis = await connect(opts);\n    const pub = await connect(opts);\n    const channel1 = \"foo\";\n    const channel2 = \"bar\";\n\n    // First subscription\n    const sub1 = await redis.subscribe(channel1);\n    const it1 = sub1.receive();\n    const promise1 = it1.next();\n    try {\n      // Second subscription\n      const sub2 = await redis.subscribe(channel2);\n      try {\n        const message = \"A\";\n        await pub.publish(channel1, message);\n        const result = await promise1;\n        assert(!result.done);\n        assertEquals(result.value, { channel: channel1, message });\n\n        const it2 = sub2.receive();\n        const promise2 = it2.next();\n        const message2 = \"B\";\n        await pub.publish(channel2, message2);\n        const result2 = await promise2;\n        assert(!result2.done);\n        assertEquals(result2.value, {\n          channel: channel2,\n          message: message2,\n        });\n      } finally {\n        sub2.close();\n      }\n    } finally {\n      pub.close();\n      sub1.close();\n      redis.close();\n    }\n  });\n}\n"
  },
  {
    "path": "tests/commands/resp3.ts",
    "content": "import {\n  assert,\n  assertArrayIncludes,\n  assertEquals,\n  assertStrictEquals,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, beforeEach, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\nimport { kUnstableProtover } from \"../../internal/symbols.ts\";\n\nexport function resp3Tests(\n  _connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  const connect = () =>\n    _connect({\n      hostname: \"127.0.0.1\",\n      port: getServer().port,\n      [kUnstableProtover]: 3,\n    });\n  beforeAll(async () => {\n    client = await connect();\n  });\n\n  afterAll(() => client.close());\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"returns a double reply as a string\", async () => {\n    client.zadd(\"key\", { one: 123, two: 2 });\n    assertEquals(await client.zscore(\"key\", \"one\"), \"123\");\n  });\n\n  it(\"returns a map reply as an array\", async () => {\n    await client.hset(\"key\", \"foo\", \"1\");\n    await client.hset(\"key\", \"bar\", \"2\");\n    assertEquals(await client.hgetall(\"key\"), [\"foo\", \"1\", \"bar\", \"2\"]);\n  });\n\n  it(\"returns a set reply as an array\", async () => {\n    await client.sadd(\"key\", \"foo\", \"1\");\n    const reply = await client.smembers(\"key\");\n    assertArrayIncludes(reply, [\"foo\", \"1\"]);\n    assertEquals(reply.length, 2);\n  });\n\n  it(\"returns a null reply as `null`\", async () => {\n    const reply = await client.get(\"no-such-key\");\n    assertStrictEquals(reply, null);\n  });\n\n  it(\"returns a boolean reply as 0 or 1\", async () => {\n    {\n      const reply = await client.eval(\"redis.setresp(3); return true\", [], []);\n      assertStrictEquals(reply, 1);\n    }\n\n    {\n      const reply = await client.eval(\"redis.setresp(3); return false\", [], []);\n      assertStrictEquals(reply, 0);\n    }\n  });\n\n  it(\"supports a verbatim string\", async () => {\n    const reply = await client.latencyDoctor();\n    assertStrictEquals(typeof reply, \"string\");\n    assert(reply.startsWith(\"txt:\"), `\"${reply}\" should start with \"txt:\"`);\n  });\n\n  it(\"supports a push reply\", async () => {\n    using client = await connect();\n    const channel = \"testing\";\n    const sub = await client.subscribe(channel);\n    const it = sub.receive();\n    const payload = \"foobar\";\n    await client.publish(channel, payload);\n    const result = await it.next();\n    assert(!result.done);\n    assertEquals(result.value, { channel, message: payload });\n  });\n\n  // deno-lint-ignore deno-lint-plugin-extra-rules/no-disabled-tests -- TODO: Support the execution of a regular command in a push-mode connection.\n  it.skip(\"supports executing a regular command in a push-mode connection\", () => {});\n\n  // deno-lint-ignore deno-lint-plugin-extra-rules/no-disabled-tests -- TODO: Currently, there is no command that returns a big number.\n  it.skip(\"supports a big number\", () => {});\n\n  // deno-lint-ignore deno-lint-plugin-extra-rules/no-disabled-tests -- TODO: Currently, there doesn't seem to be any command that returns a blob error.\n  it.skip(\"supports a blob error\", () => {});\n}\n"
  },
  {
    "path": "tests/commands/script.ts",
    "content": "import { assert, assertEquals } from \"../../deps/std/assert.ts\";\nimport { afterAll, afterEach, beforeAll, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function scriptTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n  afterAll(() => client.close());\n\n  afterEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"eval\", async () => {\n    const raw = await client.eval(\n      \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\",\n      [\"1\", \"2\"],\n      [\"3\", \"4\"],\n    );\n    assert(Array.isArray(raw));\n    assertEquals(raw, [\"1\", \"2\", \"3\", \"4\"]);\n  });\n\n  it(\"evalsha\", async () => {\n    const hash = await client.scriptLoad(\n      `return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}`,\n    );\n    try {\n      assertEquals(\n        await client.scriptExists(hash),\n        [1],\n      );\n\n      const result = await client.evalsha(hash, [\"a\", \"b\"], [\"1\", \"2\"]);\n      assert(Array.isArray(result));\n      assertEquals(result, [\"a\", \"b\", \"1\", \"2\"]);\n    } finally {\n      await client.scriptFlush();\n    }\n  });\n}\n"
  },
  {
    "path": "tests/commands/set.ts",
    "content": "import {\n  assert,\n  assertArrayIncludes,\n  assertEquals,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, beforeEach, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function setTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"sadd\", async () => {\n    assertEquals(await client.sadd(\"key\", \"1\", \"2\", \"1\"), 2);\n  });\n\n  it(\"scard\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    assertEquals(await client.scard(\"key\"), 2);\n  });\n\n  it(\"sdiff\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    await client.sadd(\"key2\", \"1\", \"3\");\n    assertArrayIncludes(await client.sdiff(\"key\", \"key2\"), [\"2\"]);\n  });\n\n  it(\"sdiffstore\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    await client.sadd(\"key2\", \"1\", \"3\");\n    assertEquals(await client.sdiffstore(\"dest\", \"key\", \"key2\"), 1);\n  });\n\n  it(\"sinter\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    await client.sadd(\"key2\", \"1\", \"3\");\n    assertArrayIncludes(await client.sinter(\"key\", \"key2\"), [\"1\"]);\n  });\n\n  it(\"sinterstore\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    await client.sadd(\"key2\", \"1\", \"3\");\n    assertEquals(await client.sinterstore(\"dest\", \"key\", \"key2\"), 1);\n  });\n\n  it(\"sismember\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    assertEquals(await client.sismember(\"key\", \"1\"), 1);\n  });\n\n  it(\"smembers\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    assertArrayIncludes(await client.smembers(\"key\"), [\"1\", \"2\"]);\n  });\n\n  it(\"smove\", async () => {\n    await client.sadd(\"key\", \"1\", \"2\");\n    assertEquals(await client.smove(\"key\", \"dest\", \"1\"), 1);\n  });\n\n  it(\"spop\", async () => {\n    await client.sadd(\"key\", \"a\");\n    const v = await client.spop(\"key\");\n    assertEquals(v, \"a\");\n  });\n\n  it(\"spop with count\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    const v = await client.spop(\"key\", 2);\n    assertArrayIncludes(v, [\"a\", \"b\"]);\n  });\n\n  it(\"srandmember\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    const v = await client.srandmember(\"key\");\n    assertArrayIncludes([\"a\", \"b\"], [v]);\n  });\n\n  it(\"srandmember with count\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    const v = await client.srandmember(\"key\", 3);\n    assertArrayIncludes([\"a\", \"b\", undefined], v);\n  });\n\n  it(\"srem\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    assertEquals(await client.srem(\"key\", \"a\"), 1);\n  });\n\n  it(\"sunion\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    await client.sadd(\"key2\", \"b\", \"c\");\n    const v = await client.sunion(\"key\", \"key2\");\n    assertArrayIncludes(v, [\"a\", \"b\", \"c\"]);\n  });\n\n  it(\"sunionstore\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    await client.sadd(\"key2\", \"b\", \"c\");\n    const v = await client.sunionstore(\"dest\", \"key\", \"key2\");\n    assertEquals(v, 3);\n  });\n\n  it(\"sscan\", async () => {\n    await client.sadd(\"key\", \"a\", \"b\");\n    const v = await client.sscan(\"key\", 0);\n    assert(Array.isArray(v));\n  });\n}\n"
  },
  {
    "path": "tests/commands/sorted_set.ts",
    "content": "import { assert, assertEquals } from \"../../deps/std/assert.ts\";\nimport type { IsExact } from \"../../deps/std/testing.ts\";\nimport {\n  afterAll,\n  assertType,\n  beforeAll,\n  beforeEach,\n  describe,\n  it,\n} from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function zsetTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"bzpopmin\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    assertEquals(await client.bzpopmin(1, \"key\"), [\"key\", \"1\", \"1\"]);\n  });\n\n  it(\"bzpopmin returns null on timeout\", async () => {\n    const arr = await client.bzpopmin(1, \"key\");\n    assertEquals(arr, null);\n  });\n\n  it(\"bzpopmax\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    assertEquals(await client.bzpopmax(1, \"key\"), [\"key\", \"2\", \"2\"]);\n  });\n\n  it(\"bzpopmax returns null on timeout\", async () => {\n    const arr = await client.bzpopmax(1, \"key\");\n    assertEquals(arr, null);\n  });\n\n  describe(\"zadd\", () => {\n    it(\"adds specified members to a sorted set\", async () => {\n      const v = await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n      assertEquals(v, 2);\n\n      const v2 = await client.zadd(\"key\", 3, \"3\");\n      assertEquals(v2, 1);\n\n      const v3 = await client.zadd(\"key\", [\n        [4, \"4\"],\n        [5, \"5\"],\n      ]);\n      assertEquals(\n        v3,\n        2,\n      );\n      assertType<IsExact<typeof v3, number>>(true);\n    });\n\n    it(\"supports `NX` and `XX`\", async () => {\n      const key = \"zaddWithNXOrXX\";\n      const v = await client.zadd(key, 1, \"1\", { nx: true });\n      assertEquals(v, 1);\n      assertType<IsExact<typeof v, number | null>>(true);\n\n      const v2 = await client.zadd(key, 2, \"1\", { mode: \"NX\" });\n      assertEquals(v2, 0); // NOTE: In RESP3, this seems to be `null`\n\n      const v3 = await client.zadd(key, 3, \"1\", { xx: true, ch: true });\n      assertEquals(v3, 1);\n      assertType<IsExact<typeof v3, number | null>>(true);\n\n      const v4 = await client.zadd(key, [[1, \"2\"]], {\n        mode: \"XX\",\n      });\n      assertEquals(v4, 0); // NOTE: In RESP3, this seems to be `null`\n    });\n\n    it(\"supports `CH`\", async () => {\n      assertEquals(await client.zadd(\"keyWithCH\", [[1, \"foo\"], [2, \"bar\"]]), 2);\n      const v = await client.zadd(\n        \"keyWithCH\",\n        { \"foo\": 1, \"bar\": 3, \"baz\": 4 },\n        { ch: true },\n      );\n      assertEquals(\n        v,\n        2,\n      );\n      assertType<IsExact<typeof v, number>>(true);\n    });\n  });\n  it(\"zaddIncr\", async () => {\n    await client.zadd(\"key\", 1, \"a\");\n    await client.zaddIncr(\"key\", 2, \"a\");\n    assertEquals(await client.zscore(\"key\", \"a\"), \"3\");\n  });\n\n  it(\"zaddIncrWithMode\", async () => {\n    assertEquals(\n      await client.zaddIncr(\"key\", 1, \"one\", { mode: \"XX\" }),\n      null,\n      \"no member should be added\",\n    );\n    assertEquals(\n      await client.zaddIncr(\"key\", 2, \"two\", { mode: \"NX\" }),\n      \"2\",\n    );\n  });\n\n  it(\"zaddIncrWithCH\", async () => {\n    await client.zadd(\"key\", 1, \"foo\");\n    assertEquals(\n      await client.zaddIncr(\"key\", 3, \"foo\", { ch: true }),\n      \"4\",\n      \"`ZADD` with `INCR` should return the new score of member\",\n    );\n    assertEquals(await client.zscore(\"key\", \"foo\"), \"4\");\n  });\n\n  it(\"zcount\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    assertEquals(await client.zcount(\"key\", 0, 1), 1);\n  });\n\n  it(\"zincrby\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    const v = await client.zincrby(\"key\", 2.0, \"1\");\n    assert(v != null);\n    assert(parseFloat(v) - 3.0 < Number.EPSILON);\n  });\n\n  it(\"zinterstore\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    await client.zadd(\"key2\", { \"1\": 1, \"3\": 3 });\n    assertEquals(await client.zinterstore(\"dest\", [\"key\", \"key2\"]), 1);\n  });\n\n  it(\"zinter\", async () => {\n    await client.zadd(\"key\", { \"1\": 1, \"2\": 2 });\n    await client.zadd(\"key2\", { \"1\": 1, \"3\": 3 });\n    assertEquals(await client.zinter([\"key\", \"key2\"]), [\"1\"]);\n    assertEquals(await client.zinter([[\"key\", 2], [\"key2\", 3]]), [\"1\"]);\n    assertEquals(\n      await client.zinter([\n        [\"key\", 2],\n        [\"key2\", 3],\n      ], { aggregate: \"MIN\" }),\n      [\"1\"],\n    );\n    assertEquals(\n      await client.zinter([\"key\", \"key2\"], {\n        withScore: true,\n      }),\n      [\"1\", \"2\"],\n    );\n  });\n\n  it(\"zlexcount\", async () => {\n    await client.zadd(\"key2\", { \"1\": 1, \"2\": 2 });\n    assertEquals(await client.zlexcount(\"key\", \"-\", \"(2\"), 0);\n  });\n\n  it(\"zpopmax\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zpopmax(\"key\", 1), [\"two\", \"2\"]);\n  });\n\n  it(\"zrange\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrange(\"key\", 1, 2), [\"two\"]);\n  });\n\n  it(\"zrangebylex\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrangebylex(\"key\", \"-\", \"(2\"), []);\n  });\n\n  it(\"zrevrangebylex\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrevrangebylex(\"key\", \"(2\", \"-\"), []);\n  });\n\n  it(\"zrangebyscore\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrangebyscore(\"key\", \"1\", \"2\"), [\"one\", \"two\"]);\n  });\n\n  it(\"zrank\", async () => {\n    await client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrank(\"key\", \"two\"), 1);\n  });\n\n  it(\"zrem\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrem(\"key\", \"one\"), 1);\n  });\n\n  it(\"zremrangebylex\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zremrangebylex(\"key\", \"[one\", \"[two\"), 2);\n  });\n\n  it(\"zremrangebyrank\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zremrangebyrank(\"key\", 1, 2), 1);\n  });\n\n  it(\"zremrangebyscore\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zremrangebyscore(\"key\", 1, 2), 2);\n  });\n\n  it(\"zrevrange\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrevrange(\"key\", 1, 2), [\"one\"]);\n  });\n\n  it(\"zrevrangebyscore\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrevrangebyscore(\"key\", 2, 1), [\"two\", \"one\"]);\n  });\n\n  it(\"zrevrank\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zrevrank(\"key\", \"one\"), 1);\n  });\n\n  it(\"zscore\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zscore(\"key\", \"one\"), \"1\");\n  });\n\n  it(\"zunionstore\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    client.zadd(\"key2\", { one: 1, three: 3 });\n    assertEquals(await client.zunionstore(\"dest\", [\"key\", \"key2\"]), 3);\n  });\n\n  it(\"zscan\", async () => {\n    client.zadd(\"key\", { one: 1, two: 2 });\n    assertEquals(await client.zscan(\"key\", 1), [\"0\", [\"one\", \"1\", \"two\", \"2\"]]);\n  });\n\n  it(\"testZrange\", async function testZrange() {\n    client.zadd(\"zrange\", 1, \"one\");\n    client.zadd(\"zrange\", 2, \"two\");\n    client.zadd(\"zrange\", 3, \"three\");\n    const v = await client.zrange(\"zrange\", 0, 1);\n    assertEquals(v, [\"one\", \"two\"]);\n  });\n\n  it(\"testZrangeWithScores\", async function testZrangeWithScores() {\n    client.zadd(\"zrangeWithScores\", 1, \"one\");\n    client.zadd(\"zrangeWithScores\", 2, \"two\");\n    client.zadd(\"zrangeWithScores\", 3, \"three\");\n    const v = await client.zrange(\"zrangeWithScores\", 0, 1, {\n      withScore: true,\n    });\n    assertEquals(v, [\"one\", \"1\", \"two\", \"2\"]);\n  });\n\n  it(\"testZrevrange\", async function testZrevrange() {\n    client.zadd(\"zrevrange\", 1, \"one\");\n    client.zadd(\"zrevrange\", 2, \"two\");\n    client.zadd(\"zrevrange\", 3, \"three\");\n    const v = await client.zrevrange(\"zrevrange\", 0, 1);\n    assertEquals(v, [\"three\", \"two\"]);\n  });\n\n  it(\n    \"testZrevrangeWithScores\",\n    async function testZrevrangeWithScores() {\n      client.zadd(\"zrevrangeWithScores\", 1, \"one\");\n      client.zadd(\"zrevrangeWithScores\", 2, \"two\");\n      client.zadd(\"zrevrangeWithScores\", 3, \"three\");\n      const v = await client.zrevrange(\"zrevrangeWithScores\", 0, 1, {\n        withScore: true,\n      });\n      assertEquals(v, [\"three\", \"3\", \"two\", \"2\"]);\n    },\n  );\n\n  it(\"testZrangebyscore\", async function testZrangebyscore() {\n    client.zadd(\"zrangebyscore\", 2, \"m1\");\n    client.zadd(\"zrangebyscore\", 5, \"m2\");\n    client.zadd(\"zrangebyscore\", 8, \"m3\");\n    client.zadd(\"zrangebyscore\", 10, \"m4\");\n    const v = await client.zrangebyscore(\"zrangebyscore\", 3, 9);\n    assertEquals(v, [\"m2\", \"m3\"]);\n  });\n\n  it(\n    \"testZrangebyscoreWithScores\",\n    async function testZrangebyscoreWithScores() {\n      client.zadd(\"zrangebyscoreWithScores\", 2, \"m1\");\n      client.zadd(\"zrangebyscoreWithScores\", 5, \"m2\");\n      client.zadd(\"zrangebyscoreWithScores\", 8, \"m3\");\n      client.zadd(\"zrangebyscoreWithScores\", 10, \"m4\");\n      const v = await client.zrangebyscore(\"zrangebyscoreWithScores\", 3, 9, {\n        withScore: true,\n      });\n      assertEquals(v, [\"m2\", \"5\", \"m3\", \"8\"]);\n    },\n  );\n\n  it(\"testZrevrangebyscore\", async function testZrevrangebyscore() {\n    client.zadd(\"zrevrangebyscore\", 2, \"m1\");\n    client.zadd(\"zrevrangebyscore\", 5, \"m2\");\n    client.zadd(\"zrevrangebyscore\", 8, \"m3\");\n    client.zadd(\"zrevrangebyscore\", 10, \"m4\");\n    const v = await client.zrevrangebyscore(\"zrevrangebyscore\", 9, 4);\n    assertEquals(v, [\"m3\", \"m2\"]);\n  });\n\n  it(\"testZrevrangebyscore\", async function testZrevrangebyscore() {\n    client.zadd(\"zrevrangebyscoreWithScores\", 2, \"m1\");\n    client.zadd(\"zrevrangebyscoreWithScores\", 5, \"m2\");\n    client.zadd(\"zrevrangebyscoreWithScores\", 8, \"m3\");\n    client.zadd(\"zrevrangebyscoreWithScores\", 10, \"m4\");\n    const v = await client.zrevrangebyscore(\n      \"zrevrangebyscoreWithScores\",\n      9,\n      4,\n      {\n        withScore: true,\n      },\n    );\n    assertEquals(v, [\"m3\", \"8\", \"m2\", \"5\"]);\n  });\n}\n"
  },
  {
    "path": "tests/commands/stream.ts",
    "content": "import type { Redis } from \"../../mod.ts\";\nimport { ErrorReplyError } from \"../../mod.ts\";\nimport { parseXId } from \"../../stream.ts\";\nimport { delay } from \"../../deps/std/async.ts\";\nimport {\n  assert,\n  assertEquals,\n  assertNotEquals,\n  assertRejects,\n} from \"../../deps/std/assert.ts\";\nimport { afterAll, beforeAll, it } from \"../../deps/std/testing.ts\";\nimport type { Connector, TestServer } from \"../test_util.ts\";\n\nexport function streamTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  const rnum = () => Math.floor(Math.random() * 1000);\n  const randomStream = () =>\n    `test-deno-${new Date().getTime()}-${rnum()}${rnum()}${rnum()}`;\n\n  const cleanupStream = async (client: Redis, ...keys: string[]) => {\n    await Promise.all(keys.map((key) => client.xtrim(key, { elements: 0 })));\n  };\n\n  const withConsumerGroup = async (\n    fn: (stream: string, group: string) => Promise<unknown>,\n  ) => {\n    const rn = Math.floor(Math.random() * 1000);\n    const stream = randomStream();\n    const group = `test-group-${rn}`;\n\n    const created = await client.xgroupCreate(stream, group, \"$\", true);\n    assertEquals(created, \"OK\");\n\n    await fn(stream, group);\n\n    assertEquals(await client.xgroupDestroy(stream, group), 1);\n  };\n\n  it(\"xadd\", async () => {\n    const key = randomStream();\n    const v = await client.xadd(key, \"*\", {\n      cat: \"what\",\n      dog: \"who\",\n      duck: \"when\",\n    });\n    assert(v != null);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xadd maxlen\", async () => {\n    const key = randomStream();\n    const v = await client.xadd(\n      key,\n      \"*\",\n      { cat: \"meow\", dog: \"woof\", duck: \"quack\" },\n      { elements: 10 },\n    );\n    assert(v != null);\n    const x = await client.xadd(\n      key,\n      \"*\",\n      { cat: \"oo\", dog: \"uu\", duck: \"pp\" },\n      { approx: false, elements: 10 },\n    );\n    assert(x != null);\n    await cleanupStream(client, key);\n  });\n\n  it(\"xreadgroup multiple streams\", async () => {\n    await withConsumerGroup(async (key, group) => {\n      const key2 = randomStream();\n\n      const created = await client.xgroupCreate(key2, group, \"$\", true);\n      assertEquals(created, \"OK\");\n\n      await Promise.all([\n        client.xadd(key, \"*\", { a: 1, b: 2 }),\n        client.xadd(key, \"*\", { a: 6, b: 7 }),\n        client.xadd(key2, \"*\", { c: \"three\", d: \"four\" }),\n      ]);\n\n      const reply = await client.xreadgroup(\n        [\n          [key, \">\"],\n          [key2, \">\"],\n        ],\n        { group, consumer: \"any\" },\n      );\n\n      assertEquals(reply.length, 2);\n      assertEquals(reply[0].key, key);\n      assertEquals(reply[0].messages.length, 2);\n      assertEquals(reply[1].key, key2);\n      assertEquals(reply[1].messages.length, 1);\n\n      // first stream cleaned up by withConsumerGroup\n      await cleanupStream(client, key2);\n    });\n  });\n\n  it(\"xread\", async () => {\n    const key = randomStream();\n    const a = await client.xadd(\n      key,\n      1000, // epoch millis only, converts to \"1000-0\" for the low-level interface to redis\n      { cat: \"moo\", dog: \"honk\", duck: \"yodel\" },\n      { elements: 10 },\n    );\n    assert(a != null);\n    const key2 = randomStream();\n\n    await client.xadd(\n      key2,\n      [1000, 0], // You may enter the ID as a numeric pair\n      { air: \"ball\", friend: \"table\" },\n      { elements: 10 },\n    );\n    const exampleMap = {\n      air: \"horn\",\n      friend: \"fiend\",\n    };\n    const c = await client.xadd(key2, [1001, 1], exampleMap, { elements: 10 });\n    assert(c != null);\n\n    const xid = 0;\n    const v = await client.xread(\n      [\n        { key, xid },\n        { key: key2, xid },\n      ],\n      { block: 5000, count: 500 },\n    );\n\n    assert(v != null);\n\n    const expectedAnimals = {\n      cat: \"moo\",\n      dog: \"honk\",\n      duck: \"yodel\",\n    };\n\n    const expectedWeird = {\n      air: \"ball\",\n      friend: \"table\",\n    };\n    const expectedOdd = {\n      air: \"horn\",\n      friend: \"fiend\",\n    };\n    assertEquals(v, [\n      {\n        key,\n        messages: [\n          {\n            xid: parseXId(\"1000-0\"),\n            fieldValues: expectedAnimals,\n          },\n        ],\n      },\n      {\n        key: key2,\n        messages: [\n          { xid: parseXId(\"1000-0\"), fieldValues: expectedWeird },\n          { xid: parseXId(\"1001-1\"), fieldValues: expectedOdd },\n        ],\n      },\n    ]);\n\n    await cleanupStream(client, key, key2);\n  });\n\n  it(\"xread manage empty stream on timeout\", async () => {\n    const key = randomStream();\n    const [stream] = await client.xread([{ key, xid: 0 }], {\n      block: 1,\n    });\n    assertEquals(stream, undefined);\n  });\n\n  it(\"xgrouphelp\", async () => {\n    const helpText = await client.xgroupHelp();\n    assert(helpText.length > 4);\n    assert(helpText[0].length > 10);\n  });\n\n  it(\"xgroup create and destroy\", async () => {\n    const groupName = \"test-group\";\n\n    const key = randomStream();\n\n    const created = await client.xgroupCreate(key, groupName, \"$\", true);\n    assertEquals(created, \"OK\");\n    await assertRejects(\n      async () => {\n        await client.xgroupCreate(key, groupName, 0, true);\n      },\n      ErrorReplyError,\n      \"-BUSYGROUP Consumer Group name already exists\",\n    );\n\n    assertEquals(await client.xgroupDestroy(key, groupName), 1);\n  });\n\n  it(\"xgroup setid and delconsumer\", async () => {\n    const key = randomStream();\n    const group = \"test-group\";\n    const consumer = \"test-consumer\";\n\n    const created = await client.xgroupCreate(key, group, \"$\", true);\n    assertEquals(created, \"OK\");\n\n    const addedId = await client.xadd(key, \"*\", { anyfield: \"anyval\" });\n\n    assert(addedId);\n\n    //  must read from a given stream to create the\n    //  consumer\n    const xid = \">\";\n    const data = await client.xreadgroup([{ key, xid }], { group, consumer });\n\n    assertEquals(data.length, 1);\n\n    assertEquals(await client.xgroupSetID(key, group, 0), \"OK\");\n\n    assertEquals(await client.xgroupDelConsumer(key, group, consumer), 1);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xreadgroup but no ack\", async () => {\n    const key = randomStream();\n    const group = \"test-group\";\n\n    const created = await client.xgroupCreate(key, group, \"$\", true);\n    assertEquals(created, \"OK\");\n\n    const addedId = await client.xadd(key, \"*\", { anyfield: \"anyval\" });\n\n    assert(addedId);\n\n    const xid = \">\";\n    const dataOut = await client.xreadgroup([{ key, xid }], {\n      group,\n      consumer: \"test-consumer\",\n    });\n\n    assertEquals(dataOut.length, 1);\n    const actualFirstStream = dataOut[0];\n    assertEquals(actualFirstStream.key, key);\n    assertEquals(actualFirstStream.messages[0].xid, addedId);\n    assertEquals(actualFirstStream.messages.length, 1);\n    assertEquals(\n      actualFirstStream.messages[0].fieldValues[\"anyfield\"],\n      \"anyval\",\n    );\n\n    // > symbol does NOT cause automatic acknowledgement by Redis\n    const ackSize = await client.xack(key, group, addedId);\n    assertEquals(ackSize, 1);\n\n    assertEquals(await client.xgroupDestroy(key, group), 1);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xack\", async () => {\n    const key = randomStream();\n    const group = \"test-group\";\n\n    const created = await client.xgroupCreate(key, group, \"$\", true);\n    assertEquals(created, \"OK\");\n\n    const addedId = await client.xadd(key, \"*\", { anyfield: \"anyval\" });\n\n    assert(addedId);\n\n    const xid = \">\";\n    // read but DO NOT auto-ack, which places\n    // the message on the PEL\n    await client.xreadgroup([{ key, xid }], {\n      group,\n      consumer: \"test-consumer\",\n    });\n\n    const acked = await client.xack(key, group, addedId);\n\n    assertEquals(acked, 1);\n\n    assertEquals(await client.xgroupDestroy(key, group), 1);\n    await cleanupStream(client, key);\n  });\n\n  it(\"xadd with map then xread\", async () => {\n    const m = new Map<string, string>();\n    m.set(\"zoo\", \"theorize\");\n    m.set(\"gable\", \"train\");\n\n    const key = randomStream();\n    const addedId = await client.xadd(key, \"*\", m);\n    assert(addedId !== null);\n\n    // one millis before now\n    const xid = addedId.unixMs - 1;\n    const v = await client.xread([{ key, xid }], { block: 5000, count: 500 });\n\n    assert(v != null);\n\n    const expectedMap = {\n      zoo: \"theorize\",\n      gable: \"train\",\n    };\n\n    assertEquals(v, [\n      {\n        key,\n        messages: [\n          {\n            xid: addedId,\n            fieldValues: expectedMap,\n          },\n        ],\n      },\n    ]);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xadd with maxlen on map then xread\", async () => {\n    const mmm = new Map<string, string>();\n    mmm.set(\"hop\", \"4\");\n    mmm.set(\"blip\", \"5\");\n\n    const key = randomStream();\n    const addedId = await client.xadd(key, \"*\", mmm, { elements: 8 });\n    assert(addedId !== null);\n\n    const justBefore = addedId.unixMs - 1;\n\n    const v = await client.xread([{ key, xid: justBefore }], {\n      block: 5000,\n      count: 500,\n    });\n\n    assert(v != null);\n\n    const expectedMap = {\n      hop: \"4\",\n      blip: \"5\",\n    };\n\n    assertEquals(v, [\n      { key, messages: [{ xid: addedId, fieldValues: expectedMap }] },\n    ]);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xdel\", async () => {\n    const key = randomStream();\n    const id0 = await client.xadd(key, \"*\", { foo: \"bar\" }, { elements: 10 });\n    const id1 = await client.xadd(key, \"*\", { foo: \"baz\" }, { elements: 10 });\n    const id2 = await client.xadd(key, \"*\", { foo: \"qux\" }, { elements: 10 });\n\n    const v = await client.xdel(key, id0, id1, id2);\n    assert(v === 3);\n    await cleanupStream(client, key);\n  });\n\n  it(\"xlen\", async () => {\n    const key = randomStream();\n    await client.xadd(key, \"*\", { foo: \"qux\" }, { elements: 5 });\n    await client.xadd(key, \"*\", { foo: \"bux\" }, { elements: 5 });\n\n    const v = await client.xlen(key);\n    assert(v === 2);\n    await cleanupStream(client, key);\n  });\n\n  it(\"unique message per consumer\", async () => {\n    await withConsumerGroup(async (key, group) => {\n      const addedIds = [];\n      const c0 = \"consumer-0\";\n      const c1 = \"consumer-1\";\n      const c2 = \"consumer-2\";\n\n      for (const consumer of [c0, c1, c2]) {\n        const payload = `data-for-${consumer}`;\n        const a = await client.xadd(key, \"*\", { target: payload });\n        assert(a);\n        addedIds.push(a);\n\n        // This special  ID means that you want all\n        // \"new\" messages in the stream.\n        const xid = \">\";\n        const data = await client.xreadgroup([{ key, xid }], {\n          group,\n          consumer,\n        });\n\n        assertEquals(data[0].messages.length, 1);\n\n        assertEquals(data[0].messages[0].fieldValues[\"target\"], payload);\n      }\n\n      await cleanupStream(client, key);\n    });\n  });\n\n  it(\"broadcast pattern, all groups read their own version of the stream\", async () => {\n    const key = randomStream();\n    const group0 = \"tg0\";\n    const group1 = \"tg1\";\n    const group2 = \"tg2\";\n    const groups = [group0, group1, group2];\n\n    for (const g of groups) {\n      const created = await client.xgroupCreate(key, g, \"$\", true);\n      assertEquals(created, \"OK\");\n    }\n\n    const addedIds = [];\n\n    let msgCount = 0;\n    for (const group of groups) {\n      const payload = `data-${msgCount}`;\n      const a = await client.xadd(key, \"*\", { target: payload });\n      assert(a);\n      addedIds.push(a);\n      msgCount++;\n\n      const consumer = \"someconsumer\";\n      const xid = \">\";\n      const data = await client.xreadgroup([{ key, xid }], {\n        group,\n        consumer,\n      });\n\n      // each group should see ALL the messages\n      // that have been emitted\n      const toCheck = data[0].messages;\n      assertEquals(toCheck.length, msgCount);\n    }\n\n    for (const g of groups) {\n      assertEquals(await client.xgroupDestroy(key, g), 1);\n    }\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xrange and xrevrange\", async () => {\n    const key = randomStream();\n    const firstId = await client.xadd(key, \"*\", { f: \"v0\" });\n    const basicResult = await client.xrange(key, \"-\", \"+\");\n    assertEquals(basicResult.length, 1);\n    assertEquals(basicResult[0].xid, firstId);\n    assertEquals(basicResult[0].fieldValues[\"f\"], \"v0\");\n\n    const secondId = await client.xadd(key, \"*\", { f: \"v1\" });\n    const revResult = await client.xrevrange(key, \"+\", \"-\");\n\n    assertEquals(revResult.length, 2);\n    assertEquals(revResult[0].xid, secondId);\n    assertEquals(revResult[0].fieldValues[\"f\"], \"v1\");\n    assertEquals(revResult[1].xid, firstId);\n    assertEquals(revResult[1].fieldValues[\"f\"], \"v0\");\n\n    // count should limit results\n    const lim = await client.xrange(key, \"-\", \"+\", 1);\n    assertEquals(lim.length, 1);\n    const revLim = await client.xrevrange(key, \"+\", \"-\", 1);\n    assertEquals(revLim.length, 1);\n\n    await cleanupStream(client, key);\n  });\n\n  it(\"xclaim and xpending, all options\", async () => {\n    await withConsumerGroup(async (key, group) => {\n      // xclaim test basic idea:\n      // 1. add messages to a group\n      // 2. then xreadgroup needs to define a consumer and read pending\n      //    messages without acking them\n      // 3. then we need to sleep 5ms and call xpending\n      // 4. from here we should be able to claim message\n      //    past the idle time and read them from a different consumer\n\n      await Promise.all([\n        client.xadd(key, 1000, { field: \"foo\" }),\n        client.xadd(key, 2000, { field: \"bar\" }),\n      ]);\n\n      const initialConsumer = \"someone\";\n\n      const firstReply = await client.xreadgroup([{ key, xid: \">\" }], {\n        group,\n        consumer: initialConsumer,\n      });\n\n      const firstPending = await client.xpending(key, group);\n\n      await delay(5);\n\n      assertEquals(firstPending.count, 2);\n      assertNotEquals(firstPending.startId, firstPending.endId);\n      assertEquals(firstPending.consumers.length, 1);\n      assertEquals(firstPending.consumers[0].name, \"someone\");\n      assertEquals(firstPending.consumers[0].pending, 2);\n\n      const minIdleTime = 4;\n\n      // minimum options\n      const claimingConsumer = \"responsible-process\";\n      const firstClaimed = await client.xclaim(\n        key,\n        { group, consumer: claimingConsumer, minIdleTime },\n        1000,\n        2000,\n      );\n      assert(firstClaimed.kind === \"messages\");\n      assertEquals(firstClaimed.messages.length, 2);\n      assertEquals(firstClaimed.messages[0].fieldValues, { field: \"foo\" });\n      assertEquals(firstClaimed.messages[1].fieldValues, { field: \"bar\" });\n\n      // ACK these messages so we can try XPENDING/XCLAIM\n      // on a new batch\n      await client.xack(\n        key,\n        group,\n        ...firstReply[0].messages.map((m) => m.xid),\n      );\n\n      // Let's write some more messages and try\n      // other formats of XPENDING/XCLAIM\n      await Promise.all([\n        client.xadd(key, 3000, { field: \"foo\" }),\n        client.xadd(key, [3000, 1], { field: \"bar\" }),\n        client.xadd(key, [3000, 2], { field: \"baz\" }),\n      ]);\n\n      const secondReply = await client.xreadgroup([{ key, xid: \">\" }], {\n        group,\n        consumer: initialConsumer,\n      });\n\n      // take a short nap and increase the lastDeliveredMs\n      await delay(5);\n\n      // try another form of xpending: counts for all consumers (we have only one)\n      const secondPending = await client.xpendingCount(key, group, {\n        start: \"-\",\n        end: \"+\",\n        count: 10,\n      });\n      assertEquals(secondPending.length, 3);\n      for (const info of secondPending) {\n        assertEquals(info.owner, \"someone\");\n        assert(info.lastDeliveredMs > 4);\n        // We called XREADGROUP so it was delivered once\n        // (but not acknowledged yet!)\n        assertEquals(info.timesDelivered, 1);\n      }\n\n      // the output for justIDs will have a different shape\n      const secondClaimedXIds = await client.xclaim(\n        key,\n        { group, consumer: claimingConsumer, minIdleTime, justXId: true },\n        [3000, 0],\n        [3000, 1],\n        [3000, 2],\n      );\n\n      assert(secondClaimedXIds.kind === \"justxid\");\n      assertEquals(secondClaimedXIds.xids, [\n        { unixMs: 3000, seqNo: 0 },\n        { unixMs: 3000, seqNo: 1 },\n        { unixMs: 3000, seqNo: 2 },\n      ]);\n\n      // ACK these messages so we can try XPENDING/XCLAIM\n      // on a new batch\n      await client.xack(\n        key,\n        group,\n        ...secondReply[0].messages.map((m) => m.xid),\n      );\n\n      // We'll try one other set of options\n      // for each of XPENDING and XCLAIM\n      await Promise.all([\n        client.xadd(key, 4000, { field: \"woof\", farm: \"chicken\" }),\n        client.xadd(key, 5000, { field: \"bop\", farm: \"duck\" }),\n      ]);\n\n      await client.xreadgroup([{ key, xid: \">\" }], {\n        group,\n        consumer: initialConsumer,\n      });\n\n      // This record won't be included in the filtered\n      // form of XPENDING, below.\n      await client.xadd(key, \"*\", { field: \"no\" });\n      await client.xreadgroup([{ key, xid: \">\" }], {\n        group,\n        consumer: \"weird-interloper\",\n      });\n\n      await delay(5);\n\n      // try another form of xpending: counts\n      // efficiently filtered down to a single consumer.\n      // We expect to see two of the three outstanding\n      // messages here, since one was claimed by\n      // weird-interloper.\n      const thirdPending = await client.xpendingCount(\n        key,\n        group,\n        { start: \"-\", end: \"+\", count: 10 },\n        \"someone\",\n      );\n      assertEquals(thirdPending.length, 2);\n      for (const info of thirdPending) {\n        assertEquals(info.owner, \"someone\");\n        assert(info.lastDeliveredMs > 4);\n        // We called XREADGROUP so it was delivered once\n        // (but not acknowledged yet!)\n        assertEquals(info.timesDelivered, 1);\n      }\n\n      // make sure all the other options can be passed to redis\n      // without some sort of disaster occurring.\n      const thirdClaimed = await client.xclaim(\n        key,\n        {\n          group,\n          consumer: claimingConsumer,\n          minIdleTime,\n          retryCount: 6,\n          force: true,\n        },\n        4000,\n        5000,\n      );\n      assert(thirdClaimed.kind === \"messages\");\n      assertEquals(thirdClaimed.messages.length, 2);\n      assertEquals(thirdClaimed.messages[0].fieldValues, {\n        field: \"woof\",\n        farm: \"chicken\",\n      });\n      assertEquals(thirdClaimed.messages[1].fieldValues, {\n        field: \"bop\",\n        farm: \"duck\",\n      });\n    });\n  });\n\n  it(\"xinfo\", async () => {\n    await withConsumerGroup(async (key, group) => {\n      await client.xadd(key, 1, { hello: \"no\" });\n      await client.xadd(key, 2, { hello: \"yes\" });\n\n      const basicStreamInfo = await client.xinfoStream(key);\n      assertEquals(basicStreamInfo.length, 2);\n      assertEquals(basicStreamInfo.groups, 1);\n      assert(basicStreamInfo.radixTreeKeys > 0);\n      assert(basicStreamInfo.radixTreeNodes > 0);\n      assertEquals(basicStreamInfo.lastGeneratedId, { unixMs: 2, seqNo: 0 });\n      assertEquals(basicStreamInfo.firstEntry, {\n        xid: { unixMs: 1, seqNo: 0 },\n        fieldValues: { hello: \"no\" },\n      });\n      assertEquals(basicStreamInfo.lastEntry, {\n        xid: { unixMs: 2, seqNo: 0 },\n        fieldValues: { hello: \"yes\" },\n      });\n\n      // Let's do an XREADGROUP so that we see some entries in the PEL\n      const _ = client.xreadgroup([[key, \">\"]], { group, consumer: \"someone\" });\n\n      const fullStreamInfo = await client.xinfoStreamFull(key);\n      assertEquals(fullStreamInfo.length, 2);\n      assert(fullStreamInfo.radixTreeKeys > 0);\n      assert(fullStreamInfo.radixTreeNodes > 0);\n      assertEquals(fullStreamInfo.groups.length, 1);\n      assertEquals(fullStreamInfo.groups[0].consumers.length, 1);\n\n      const cc = fullStreamInfo.groups[0].consumers[0];\n      assertEquals(cc.name, \"someone\");\n      assert(cc.seenTime > 0);\n      assertEquals(cc.pelCount, 2);\n      assertEquals(cc.pending.length, 2);\n      for (const msg of cc.pending) {\n        assertEquals(msg.timesDelivered, 1);\n      }\n      assertEquals(fullStreamInfo.entries.length, 2);\n\n      const limitWithCount = await client.xinfoStreamFull(key, 1);\n      assertEquals(limitWithCount.length, 2);\n      assert(limitWithCount.radixTreeKeys > 0);\n      assert(limitWithCount.radixTreeNodes > 0);\n      assertEquals(limitWithCount.groups.length, 1);\n      assertEquals(limitWithCount.groups[0].consumers.length, 1);\n\n      const c = limitWithCount.groups[0].consumers[0];\n      assertEquals(c.name, \"someone\");\n      assert(c.seenTime > 0);\n      assertEquals(c.pelCount, 2);\n      // The COUNT option limits this array to a single entry!\n      assertEquals(c.pending.length, 1);\n      for (const msg of c.pending) {\n        assertEquals(msg.timesDelivered, 1);\n      }\n      // The COUNT option limits this array to a single entry!\n      assertEquals(limitWithCount.entries.length, 1);\n\n      // Let's make another group and see more stats\n      await client.xgroupCreate(key, \"newgroup\", \"$\", true);\n\n      const groupInfos = await client.xinfoGroups(key);\n      assertEquals(groupInfos.length, 2);\n\n      const newGroup = groupInfos.find((g) => g.name === \"newgroup\");\n      const oldGroup = groupInfos.find((g) => g.name === group);\n      assert(newGroup);\n      assert(oldGroup);\n\n      assertEquals(oldGroup.pending, 2);\n      assertEquals(newGroup.pending, 0);\n\n      // Add one more record and read it with a new consumer,\n      // so that we can check the parsing of deno-redis xinfo_consumers\n      await client.xadd(key, \"*\", { hello: \"maybe\" });\n      await client.xreadgroup([[key, \">\"]], { group, consumer: \"newbie\" });\n\n      // Increase the idle time by falling asleep\n      await delay(2);\n      const consumerInfos = await client.xinfoConsumers(key, group);\n      assertEquals(consumerInfos.length, 2);\n      const newConsumer = consumerInfos.find((c) => c.name === \"newbie\");\n      const oldConsumer = consumerInfos.find((c) => c.name === \"someone\");\n      assert(newConsumer);\n      assert(oldConsumer);\n      assert(newConsumer.idle > 1);\n      assert(oldConsumer.idle > 1);\n      // New consumer read one message with \">\"\n      assertEquals(newConsumer.pending, 1);\n      // Old consumer read two messages with \">\"\n      assertEquals(oldConsumer.pending, 2);\n\n      assertEquals(await client.xgroupDestroy(key, \"newgroup\"), 1);\n    });\n  });\n}\n"
  },
  {
    "path": "tests/commands/string.ts",
    "content": "import {\n  assertEquals,\n  assertGreater,\n  assertLessOrEqual,\n} from \"../../deps/std/assert.ts\";\nimport type { IsExact, IsNullable } from \"../../deps/std/testing.ts\";\nimport {\n  afterAll,\n  assertType,\n  beforeAll,\n  beforeEach,\n  describe,\n  it,\n} from \"../../deps/std/testing.ts\";\nimport {\n  type Connector,\n  type TestServer,\n  usesRedisVersion,\n} from \"../test_util.ts\";\nimport type { Redis } from \"../../mod.ts\";\n\nexport function stringTests(\n  connect: Connector,\n  getServer: () => TestServer,\n): void {\n  let client!: Redis;\n  beforeAll(async () => {\n    const server = getServer();\n    client = await connect({ hostname: \"127.0.0.1\", port: server.port });\n  });\n\n  afterAll(() => client.close());\n\n  beforeEach(async () => {\n    await client.flushdb();\n  });\n\n  it(\"append\", async () => {\n    await client.set(\"key\", \"foo\");\n    const rep = await client.append(\"key\", \"bar\");\n    assertEquals(rep, 6);\n    const v = await client.get(\"key\");\n    assertEquals(v, \"foobar\");\n  });\n\n  it(\"bitcount\", async () => {\n    await client.set(\"key\", \"foo\"); // 01100110 01101111 01101111\n    const v = await client.bitcount(\"key\");\n    assertEquals(v, 16);\n  });\n\n  it(\"bitfieldWithoutOperations\", async () => {\n    await client.set(\"key\", \"test\");\n    const v = await client.bitfield(\"key\");\n    assertEquals(v, []);\n  });\n\n  it(\"bitfield with opts\", async () => {\n    await client.set(\"key\", \"4660\");\n    const v = await client.bitfield(\"key\", {\n      get: { type: \"u8\", offset: 0 },\n      set: { type: \"i5\", offset: 1, value: 0 },\n      incrby: { type: \"u16\", offset: 2, increment: 2 },\n    });\n    assertEquals(v, [52, 13, 218]);\n  });\n\n  it(\"bitfield with overflow\", async () => {\n    const v = await client.bitfield(\"key\", {\n      overflow: \"FAIL\",\n    });\n    assertEquals(v, []);\n  });\n\n  it(\"bitop\", async () => {\n    await client.set(\"key1\", \"foo\"); // 01100110 01101111 01101111\n    await client.set(\"key2\", \"bar\"); // 01100010 01100001 01110010\n    await client.bitop(\"AND\", \"dest\", \"key1\", \"key2\");\n    const v = await client.get(\"dest\");\n    assertEquals(v, \"bab\"); // 01100010 01100001 01100000\n  });\n\n  it(\"bitpos\", async () => {\n    await client.set(\"key\", \"2\"); // 00110010\n    assertEquals(await client.bitpos(\"key\", 0), 0);\n    assertEquals(await client.bitpos(\"key\", 1), 2);\n  });\n\n  it(\"decr\", async () => {\n    const rep = await client.decr(\"key\");\n    assertEquals(rep, -1);\n    assertEquals(await client.get(\"key\"), \"-1\");\n  });\n\n  it(\"decby\", async () => {\n    const rep = await client.decrby(\"key\", 101);\n    assertEquals(rep, -101);\n    assertEquals(await client.get(\"key\"), \"-101\");\n  });\n\n  it(\"getWhenNil\", async () => {\n    const hoge = await client.get(\"none\");\n    assertEquals(hoge, null);\n  });\n\n  it(\"getbit\", async () => {\n    await client.set(\"key\", \"3\"); // 00110011\n    assertEquals(await client.getbit(\"key\", 0), 0);\n    assertEquals(await client.getbit(\"key\", 2), 1);\n  });\n\n  it(\"getrange\", async () => {\n    await client.set(\"key\", \"Hello world!\");\n    const v = await client.getrange(\"key\", 6, 10);\n    assertEquals(v, \"world\");\n  });\n\n  it(\"getset\", async function testGetSet() {\n    await client.set(\"key\", \"val\");\n    const v = await client.getset(\"key\", \"lav\");\n    assertEquals(v, \"val\");\n    assertEquals(await client.get(\"key\"), \"lav\");\n  });\n\n  it(\"incr\", async () => {\n    const rep = await client.incr(\"key\");\n    assertEquals(rep, 1);\n    assertEquals(await client.get(\"key\"), \"1\");\n  });\n\n  it(\"incrby\", async () => {\n    const rep = await client.incrby(\"key\", 101);\n    assertEquals(rep, 101);\n    assertEquals(await client.get(\"key\"), \"101\");\n  });\n\n  it(\"incrbyfloat\", async () => {\n    await client.set(\"key\", \"2.1\");\n    const v = await client.incrbyfloat(\"key\", 0.5);\n    assertEquals(v, \"2.6\");\n    assertEquals(await client.get(\"key\"), \"2.6\");\n  });\n\n  it(\"mget\", async () => {\n    await client.set(\"key1\", \"val1\");\n    await client.set(\"key2\", \"val2\");\n    await client.set(\"key3\", \"val3\");\n    const v = await client.mget(\"key1\", \"key2\", \"key3\");\n    assertEquals(v, [\"val1\", \"val2\", \"val3\"]);\n  });\n\n  it(\"mset\", async () => {\n    let rep = await client.mset(\"key1\", \"foo\");\n    assertEquals(rep, \"OK\");\n    rep = await client.mset({ key2: \"bar\", key3: \"baz\" });\n    assertEquals(rep, \"OK\");\n    rep = await client.mset([\"key4\", \"bar\"], [\"key5\", \"baz\"]);\n    assertEquals(rep, \"OK\");\n    assertEquals(await client.get(\"key1\"), \"foo\");\n    assertEquals(await client.get(\"key2\"), \"bar\");\n    assertEquals(await client.get(\"key3\"), \"baz\");\n  });\n\n  it(\"msetnx\", async () => {\n    let rep1 = await client.msetnx(\"key1\", \"foo\");\n    assertEquals(rep1, 1); // All the keys were set.\n    rep1 = await client.msetnx({ key2: \"bar\" });\n    assertEquals(rep1, 1); // All the keys were set.\n    rep1 = await client.msetnx([\"key4\", \"bar\"], [\"key5\", \"baz\"]);\n    assertEquals(rep1, 1);\n    const rep2 = await client.msetnx({ key2: \"baz\", key3: \"qux\" });\n    assertEquals(rep2, 0); // No key was set.\n    assertEquals(await client.get(\"key1\"), \"foo\");\n    assertEquals(await client.get(\"key2\"), \"bar\");\n    assertEquals(await client.get(\"key3\"), null);\n  });\n\n  it(\"psetex\", async () => {\n    const rep = await client.psetex(\"key1\", 1000, \"test\");\n    assertEquals(rep, \"OK\");\n    assertEquals(await client.get(\"key1\"), \"test\");\n  });\n\n  describe(\"set\", () => {\n    it(\"can set a key\", async () => {\n      const s = await client.set(\"key\", \"fuga你好こんにちは\");\n      assertEquals(s, \"OK\");\n      assertType<IsExact<typeof s, string>>(true);\n      const fuga = await client.get(\"key\");\n      assertEquals(fuga, \"fuga你好こんにちは\");\n    });\n\n    it(\"supports `Number`\", async () => {\n      const s = await client.set(\"key\", 123);\n      assertEquals(s, \"OK\");\n      const v = await client.get(\"key\");\n      assertEquals(v, \"123\");\n    });\n\n    it(\"supports `GET` option\", async () => {\n      const prev = \"foobar\";\n      await client.set(\"setWithGetOpt\", prev);\n      const result = await client.set(\"setWithGetOpt\", \"baz\", { get: true });\n      assertEquals(result, prev);\n      assertType<IsNullable<typeof result>>(true);\n    });\n\n    it(\"supports `NX` option\", async () => {\n      const prev = \"foo\";\n      const v1 = await client.set(\"setWithNX\", prev, { mode: \"NX\" });\n      assertEquals(v1, \"OK\");\n      assertType<IsNullable<typeof v1>>(true);\n\n      const v2 = await client.set(\"setWithNX\", \"bar\", { nx: true });\n      assertEquals(v2, null);\n      assertType<IsNullable<typeof v2>>(true);\n    });\n\n    it(\"supports `XX` option\", async () => {\n      const v1 = await client.set(\"setWithXX\", \"foo\", { mode: \"XX\" });\n      assertEquals(v1, null);\n      assertType<IsNullable<typeof v1>>(true);\n\n      const v2 = await client.set(\"setWithXX\", \"foo\", { xx: true });\n      assertEquals(v2, null);\n      assertType<IsNullable<typeof v2>>(true);\n    });\n\n    it(\"supports `EXAT` option\", async () => {\n      const date = new Date();\n      date.setDate(date.getDate() + 1);\n      await client.set(\"setWithEXAT\", \"foo\", {\n        exat: Math.floor(date.valueOf() / 1000),\n      });\n      const ttl = await client.ttl(\"setWithEXAT\");\n      const oneDay = 86400;\n      assertLessOrEqual(ttl, oneDay);\n      assertGreater(ttl, oneDay - 3);\n    });\n\n    it(\"supports `PXAT` option\", async () => {\n      const date = new Date();\n      date.setDate(date.getDate() + 1);\n      await client.set(\"setWithPXAT\", \"bar\", { pxat: date.valueOf() });\n      const ttl = await client.ttl(\"setWithPXAT\");\n      const oneDay = 86400;\n      assertLessOrEqual(ttl, oneDay);\n      assertGreater(ttl, oneDay - 3);\n    });\n  });\n\n  it(\"setbit\", async () => {\n    await client.set(\"key\", \"2\"); // 00110010\n    assertEquals(\n      0,\n      await client.setbit(\"key\", 1, \"1\"), // 01110010\n    );\n    assertEquals(\n      1,\n      await client.setbit(\"key\", 3, \"0\"), // 01100010 => b\n    );\n    const v = await client.get(\"key\");\n    assertEquals(v, \"b\");\n  });\n\n  it(\"setex\", async () => {\n    const rep = await client.setex(\"key\", 1, \"test\");\n    assertEquals(rep, \"OK\");\n    assertEquals(await client.get(\"key\"), \"test\");\n  });\n\n  it(\"setnx\", async () => {\n    assertEquals(await client.setnx(\"key\", \"foo\"), 1);\n    assertEquals(await client.setnx(\"key\", \"bar\"), 0);\n    const v = await client.get(\"key\");\n    assertEquals(v, \"foo\");\n  });\n\n  it(\"setrange\", async () => {\n    await client.set(\"key\", \"Hello, Deno!\");\n    const rep = await client.setrange(\"key\", 7, \"Redis!\");\n    assertEquals(rep, 13);\n    const v = await client.get(\"key\");\n    assertEquals(v, \"Hello, Redis!\");\n  });\n\n  it(\"stralgo\", {\n    // NOTE(#454): STRALGO has been dropped\n    ignore: !usesRedisVersion(\"6\"),\n  }, async () => {\n    await client.set(\"a\", \"Hello\");\n    await client.set(\"b\", \"Deno!\");\n    const matches = [\n      [\n        [4, 4],\n        [3, 3],\n      ],\n      [\n        [1, 1],\n        [1, 1],\n      ],\n    ];\n    const matchesWithLen = [\n      [\n        [4, 4],\n        [3, 3],\n        1,\n      ],\n      [\n        [1, 1],\n        [1, 1],\n        1,\n      ],\n    ];\n    assertEquals(await client.stralgo(\"LCS\", \"KEYS\", \"a\", \"b\"), \"eo\");\n    assertEquals(\n      await client.stralgo(\"LCS\", \"KEYS\", \"a\", \"b\", { len: true }),\n      2,\n    );\n    assertEquals(\n      await client.stralgo(\"LCS\", \"KEYS\", \"a\", \"b\", { idx: true }),\n      [\"matches\", matches, \"len\", 2],\n    );\n    assertEquals(\n      await client.stralgo(\n        \"LCS\",\n        \"KEYS\",\n        \"a\",\n        \"b\",\n        { idx: true, withmatchlen: true },\n      ),\n      [\"matches\", matchesWithLen, \"len\", 2],\n    );\n    assertEquals(\n      await client.stralgo(\n        \"LCS\",\n        \"KEYS\",\n        \"a\",\n        \"b\",\n        { idx: true, minmatchlen: 2 },\n      ),\n      [\"matches\", [], \"len\", 2],\n    );\n    assertEquals(\n      await client.stralgo(\"LCS\", \"STRINGS\", \"Hello\", \"Deno!\"),\n      \"eo\",\n    );\n    assertEquals(\n      await client.stralgo(\"LCS\", \"STRINGS\", \"Hello\", \"Deno!\", { len: true }),\n      2,\n    );\n  });\n\n  it(\"strlen\", async () => {\n    await client.set(\"key\", \"foobar\");\n    const v = await client.strlen(\"key\");\n    assertEquals(v, 6);\n  });\n}\n"
  },
  {
    "path": "tests/commands_test.ts",
    "content": "import { nextPort, startRedis, stopRedis } from \"./test_util.ts\";\nimport type { TestServer } from \"./test_util.ts\";\nimport { aclTests } from \"./commands/acl.ts\";\nimport { connectionTests } from \"./commands/connection.ts\";\nimport { generalTests } from \"./commands/general.ts\";\nimport { geoTests } from \"./commands/geo.ts\";\nimport { hashTests } from \"./commands/hash.ts\";\nimport { hyperloglogTests } from \"./commands/hyper_loglog.ts\";\nimport { keyTests } from \"./commands/key.ts\";\nimport { listTests } from \"./commands/list.ts\";\nimport { pipelineTests } from \"./commands/pipeline.ts\";\nimport { pubsubTests } from \"./commands/pubsub.ts\";\nimport { setTests } from \"./commands/set.ts\";\nimport { zsetTests } from \"./commands/sorted_set.ts\";\nimport { scriptTests } from \"./commands/script.ts\";\nimport { streamTests } from \"./commands/stream.ts\";\nimport { stringTests } from \"./commands/string.ts\";\nimport { latencyTests } from \"./commands/latency.ts\";\nimport { resp3Tests } from \"./commands/resp3.ts\";\nimport { connect } from \"../redis.ts\";\nimport { connect as connectWebStreamsConnection } from \"../experimental/web_streams_connection/mod.ts\";\n\nimport { afterAll, beforeAll, describe } from \"../deps/std/testing.ts\";\n\ndescribe(\"commands\", () => {\n  let port!: number;\n  let server!: TestServer;\n  beforeAll(async () => {\n    port = nextPort();\n    server = await startRedis({ port });\n  });\n  afterAll(() => stopRedis(server));\n\n  const getServer = () => server;\n\n  for (\n    const [kind, connector] of [\n      [\"deno_streams connection\", connect] as const,\n      [\n        \"experimental web_streams connection\",\n        connectWebStreamsConnection,\n      ] as const,\n    ]\n  ) {\n    describe(kind, () => {\n      describe(\"acl\", () => aclTests(connector, getServer));\n      describe(\"connection\", () => connectionTests(connector, getServer));\n      describe(\"general\", () => generalTests(connector, getServer));\n      describe(\"geo\", () => geoTests(connector, getServer));\n      describe(\"hash\", () => hashTests(connector, getServer));\n      describe(\"hyperloglog\", () => hyperloglogTests(connector, getServer));\n      describe(\"key\", () => keyTests(connector, getServer));\n      describe(\"list\", () => listTests(connector, getServer));\n      describe(\"pipeline\", () => pipelineTests(connector, getServer));\n      describe(\"pubsub\", () => pubsubTests(connector, getServer));\n      describe(\"set\", () => setTests(connector, getServer));\n      describe(\"zset\", () => zsetTests(connector, getServer));\n      describe(\"script\", () => scriptTests(connector, getServer));\n      describe(\"stream\", () => streamTests(connector, getServer));\n      describe(\"string\", () => stringTests(connector, getServer));\n      describe(\"latency\", () => latencyTests(connector, getServer));\n      describe(\"RESP3\", () => resp3Tests(connector, getServer));\n    });\n  }\n});\n"
  },
  {
    "path": "tests/pool_test.ts",
    "content": "import type {\n  DefaultPubSubMessageType,\n  RedisSubscription,\n} from \"../subscription.ts\";\nimport { createPoolClient } from \"../pool/mod.ts\";\nimport { assert, assertEquals } from \"../deps/std/assert.ts\";\nimport { afterAll, beforeAll, describe, it } from \"../deps/std/testing.ts\";\nimport type { TestServer } from \"./test_util.ts\";\nimport { nextPort, startRedis, stopRedis } from \"./test_util.ts\";\n\ndescribe(\"createPoolClient\", () => {\n  let port!: number;\n  let server!: TestServer;\n  beforeAll(async () => {\n    port = nextPort();\n    server = await startRedis({ port });\n  });\n  afterAll(() => stopRedis(server));\n\n  it(\"supports distributing commands to pooled connections\", async () => {\n    const client = await createPoolClient({\n      connection: {\n        hostname: \"127.0.0.1\",\n        port,\n      },\n    });\n    try {\n      const blpopPromise = client.blpop(500, \"list\");\n      setTimeout(() => {\n        client.lpush(\"list\", \"foo\");\n      }, 100);\n      const existsPromise = client.exists(\"list\");\n      const replies = await Promise.all([\n        blpopPromise,\n        existsPromise,\n      ]);\n      assertEquals(\n        replies,\n        [[\"list\", \"foo\"], 0],\n        \"BLPOP should not block subsequent EXISTS\",\n      );\n    } finally {\n      await client.flushdb();\n      client.close();\n    }\n  });\n\n  it(\"supports Pub/Sub\", async () => {\n    const client = await createPoolClient({\n      connection: {\n        hostname: \"127.0.0.1\",\n        port,\n      },\n    });\n    let subscription: RedisSubscription<DefaultPubSubMessageType> | null = null;\n    try {\n      const channel = \"pool_test\";\n      subscription = await client.subscribe(channel);\n      const payload = \"foobar\";\n      await client.publish(channel, payload);\n      const iter = subscription.receive();\n      const message = await iter.next();\n      assert(!message.done);\n      assertEquals(message.value, {\n        channel,\n        message: payload,\n      });\n    } finally {\n      subscription?.close();\n      await client.flushdb();\n      client.close();\n    }\n  });\n});\n"
  },
  {
    "path": "tests/reconnect_test.ts",
    "content": "import {\n  assert,\n  assertEquals,\n  assertInstanceOf,\n  assertStringIncludes,\n} from \"../deps/std/assert.ts\";\nimport { beforeAll, describe, it } from \"../deps/std/testing.ts\";\nimport {\n  newClient,\n  nextPort,\n  startRedis,\n  stopRedis,\n  withTimeout,\n} from \"./test_util.ts\";\n\ndescribe(\"auto reconnection\", () => {\n  let port!: number;\n  beforeAll(() => {\n    port = nextPort();\n  });\n\n  it(\n    \"supports auto reconnection\",\n    withTimeout(10000, async () => {\n      await using server = await startRedis({ port });\n      using client = await newClient({ hostname: \"127.0.0.1\", port });\n      assertEquals(await client.ping(), \"PONG\");\n      await stopRedis(server);\n      let reconnectingFired = 0;\n      client.addEventListener(\"reconnecting\", (e) => {\n        reconnectingFired++;\n        assertInstanceOf(e, CustomEvent);\n      });\n      let connectFired = 0;\n      client.addEventListener(\"connect\", (e) => {\n        connectFired++;\n        assertInstanceOf(e, CustomEvent);\n      });\n      await using server2 = await startRedis({ port });\n      assertEquals(await client.ping(), \"PONG\");\n      client.close();\n      await stopRedis(server2);\n      assertEquals(reconnectingFired, 1);\n      assertEquals(connectFired, 1);\n    }),\n  );\n\n  it(\n    \"supports auto reconnection with db spec\",\n    withTimeout(10000, async () => {\n      // Regression test for https://github.com/denodrivers/redis/issues/430\n      await using server = await startRedis({ port });\n      using client = await newClient({\n        hostname: \"127.0.0.1\",\n        port,\n        db: 1,\n        backoff: () => 100,\n      });\n      assertEquals(await client.ping(), \"PONG\");\n      await stopRedis(server);\n      await using server2 = await startRedis({ port });\n      assertEquals(await client.ping(), \"PONG\");\n      const clientInfo = await client.clientInfo();\n      assert(clientInfo);\n      assertStringIncludes(clientInfo, \"db=1\");\n      client.close();\n      await stopRedis(server2);\n    }),\n  );\n});\n"
  },
  {
    "path": "tests/server/redis.conf",
    "content": "# Base configuration\ndaemonize no\nappendonly yes\ncluster-node-timeout 30000\nmaxclients 1001\n"
  },
  {
    "path": "tests/test_util.ts",
    "content": "import type { Redis, RedisConnectOptions } from \"../mod.ts\";\nimport { connect } from \"../mod.ts\";\nimport { delay } from \"../deps/std/async.ts\";\n\nexport type Connector = typeof connect;\ninterface Logger {\n  info(message: string): void;\n}\nexport interface TestServer {\n  path: string;\n  port: number;\n  process: Deno.ChildProcess;\n  logger: Logger;\n  [Symbol.asyncDispose](): Promise<void>;\n}\n\nconst encoder = new TextEncoder();\n\nexport async function startRedis({\n  port = 6379,\n  clusterEnabled = false,\n  makeClusterConfigFile = false,\n  logger = console,\n}): Promise<TestServer> {\n  const path = tempPath(String(port));\n  if (!(await exists(path))) {\n    await Deno.mkdir(path);\n  }\n\n  // Setup redis.conf\n  const destPath = `${path}/redis.conf`;\n  let config = await Deno.readTextFile(\"tests/server/redis.conf\");\n  config += `dir ${path}\\nport ${port}\\n`;\n  if (clusterEnabled) {\n    config += \"cluster-enabled yes\\n\";\n    if (makeClusterConfigFile) {\n      const clusterConfigFile = `${path}/cluster.conf`;\n      config += `cluster-config-file ${clusterConfigFile}`;\n    }\n  }\n  await Deno.writeFile(destPath, encoder.encode(config));\n\n  // Start a redis server\n  logger.info(`Start a redis server at port ${port}`);\n  const process = new Deno.Command(\"redis-server\", {\n    args: [`${path}/redis.conf`],\n    stdin: \"null\",\n    stdout: \"null\",\n    stderr: \"piped\",\n  }).spawn();\n\n  await waitForPort(port);\n  const testServer = {\n    path,\n    port,\n    process,\n    logger,\n    [Symbol.asyncDispose]: () => {\n      return stopRedis(testServer);\n    },\n  };\n  return testServer;\n}\n\nexport async function stopRedis(server: TestServer): Promise<void> {\n  try {\n    await Deno.remove(server.path, { recursive: true });\n  } catch (error) {\n    if (!(error instanceof Deno.errors.NotFound)) {\n      throw error;\n    }\n  }\n\n  server.logger.info(`Stop a redis server running in port ${server.port}`);\n  await ensureTerminated(server.process);\n}\n\nexport async function ensureTerminated(\n  process: Deno.ChildProcess,\n): Promise<void> {\n  try {\n    await process.stderr.cancel();\n    process.kill(\"SIGKILL\");\n    await process.status;\n  } catch (error) {\n    const alreadyKilled = error instanceof TypeError &&\n      error.message === \"Child process has already terminated\";\n    if (alreadyKilled) {\n      return;\n    }\n    throw error;\n  }\n}\n\nexport function newClient(opt: RedisConnectOptions): Promise<Redis> {\n  return connect(opt);\n}\n\nasync function exists(path: string): Promise<boolean> {\n  try {\n    await Deno.stat(path);\n    return true;\n  } catch (err) {\n    if (err instanceof Deno.errors.NotFound) {\n      return false;\n    }\n    throw err;\n  }\n}\n\nlet currentPort = 7000;\nexport function nextPort(): number {\n  return currentPort++;\n}\n\nasync function waitForPort(port: number): Promise<void> {\n  let retries = 0;\n  const maxRetries = 5;\n  while (true) {\n    try {\n      const conn = await Deno.connect({ port });\n      conn.close();\n      break;\n    } catch (e) {\n      retries++;\n      if (retries === maxRetries) {\n        throw e;\n      }\n      await delay(200);\n    }\n  }\n}\n\nfunction tempPath(fileName: string): string {\n  const url = new URL(`./tmp/${fileName}`, import.meta.url);\n  return url.pathname;\n}\n\nexport function usesRedisVersion(version: \"6\" | \"7\" | \"8\"): boolean {\n  const redisVersion = Deno.env.get(\"REDIS_VERSION\");\n  if (redisVersion == null) return false;\n  return redisVersion.startsWith(`${version}.`) || redisVersion === version;\n}\n\nexport function withTimeout(\n  timeout: number,\n  thunk: () => Promise<void>,\n): () => Promise<void> {\n  async function thunkWithTimeout(): Promise<void> {\n    const { resolve, reject, promise } = Promise.withResolvers<void>();\n    const timer = setTimeout(() => {\n      reject(new Error(\"Timeout\"));\n    }, timeout);\n    try {\n      await Promise.race([\n        thunk().then(() => resolve()),\n        promise,\n      ]);\n    } finally {\n      clearTimeout(timer);\n    }\n  }\n  return thunkWithTimeout;\n}\n"
  },
  {
    "path": "tests/util_test.ts",
    "content": "import { parseURL } from \"../mod.ts\";\nimport { assertEquals } from \"../deps/std/assert.ts\";\nimport { describe, it } from \"../deps/std/testing.ts\";\n\ndescribe(\"util\", {\n  permissions: \"none\",\n}, () => {\n  describe(\"parseURL\", () => {\n    it(\"parses basic URL\", () => {\n      const options = parseURL(\"redis://127.0.0.1:7003\");\n      assertEquals(options.hostname, \"127.0.0.1\");\n      assertEquals(options.port, 7003);\n      assertEquals(options.tls, false);\n      assertEquals(options.db, undefined);\n      assertEquals(options.name, undefined);\n      assertEquals(options.password, undefined);\n    });\n\n    it(\"parses complex URL\", () => {\n      const options = parseURL(\"rediss://username:password@127.0.0.1:7003/1\");\n      assertEquals(options.hostname, \"127.0.0.1\");\n      assertEquals(options.port, 7003);\n      assertEquals(options.tls, true);\n      assertEquals(options.db, 1);\n      assertEquals(options.name, \"username\");\n      assertEquals(options.password, \"password\");\n    });\n\n    it(\"parses URL with search options\", () => {\n      const options = parseURL(\n        \"redis://127.0.0.1:7003/?db=2&password=password&ssl=true\",\n      );\n      assertEquals(options.hostname, \"127.0.0.1\");\n      assertEquals(options.port, 7003);\n      assertEquals(options.tls, true);\n      assertEquals(options.db, 2);\n      assertEquals(options.name, undefined);\n      assertEquals(options.password, \"password\");\n    });\n\n    it(\"checks parameter parsing priority\", () => {\n      const options = parseURL(\n        \"rediss://username:password@127.0.0.1:7003/1?db=2&password=password2&ssl=false\",\n      );\n      assertEquals(options.hostname, \"127.0.0.1\");\n      assertEquals(options.port, 7003);\n      assertEquals(options.tls, true);\n      assertEquals(options.db, 1);\n      assertEquals(options.name, \"username\");\n      assertEquals(options.password, \"password\");\n    });\n  });\n});\n"
  },
  {
    "path": "tools/format-benchmark-results.js",
    "content": "// deno-lint-ignore-file no-console -- This file is a script, not a library module.\nimport { join } from \"node:path\";\n\nfunction formatResultsAsMarkdown({ name, results }) {\n  const detailKeys = [\"min\", \"max\", \"mean\", \"median\"];\n  const header = [\"name\", \"ops\", \"margin\", ...detailKeys, \"samples\"];\n  const rows = results.map((result) => {\n    const { name, ops, margin, details, samples } = result;\n    return [\n      name,\n      ops,\n      margin,\n      ...detailKeys.map((key) => details[key]),\n      samples,\n    ];\n  });\n  const table = [\n    header,\n    header.map(() => \":---:\"),\n    ...rows,\n  ]\n    .map(makeTableRow)\n    .join(\"\\n\");\n  return `## ${name}\\n\\n${table}\\n`;\n}\n\nfunction makeTableRow(columns) {\n  return `|${columns.join(\"|\")}|`;\n}\n\nconst resultsDir = new URL(\"../tmp/benchmark\", import.meta.url);\nconst paths = Array.from(Deno.readDirSync(resultsDir)).map((x) =>\n  join(resultsDir.pathname, x.name)\n).sort();\nfor (const path of paths) {\n  const results = JSON.parse(await Deno.readTextFile(path));\n  const markdown = formatResultsAsMarkdown(results);\n  console.log(markdown);\n}\n"
  }
]