Repository: denodrivers/redis Branch: master Commit: 1e72e0bafc47 Files: 106 Total size: 357.0 KB Directory structure: gitextract_mcsn30us/ ├── .denov ├── .editorconfig ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── pinact.yaml │ └── workflows/ │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .octocov.yml ├── LICENSE ├── README.md ├── backoff.ts ├── benchmark/ │ ├── .npmrc │ ├── benchmark.js │ ├── deno-redis.ts │ ├── ioredis.js │ └── package.json ├── client.ts ├── command.ts ├── connection.ts ├── default_client.ts ├── default_connection.ts ├── default_subscription.ts ├── deno.json ├── deps/ │ ├── cluster-key-slot.js │ └── std/ │ ├── assert.ts │ ├── async.ts │ ├── bytes.ts │ ├── collections.ts │ ├── io.ts │ ├── random.ts │ └── testing.ts ├── errors.ts ├── events.ts ├── executor.ts ├── experimental/ │ ├── README.md │ ├── cluster/ │ │ ├── README.md │ │ └── mod.ts │ ├── pool/ │ │ └── mod.ts │ └── web_streams_connection/ │ └── mod.ts ├── import_map.dev.json ├── import_map.test.json ├── internal/ │ ├── buffered_readable_stream.ts │ ├── buffered_readable_stream_test.ts │ ├── concate_bytes.ts │ ├── concate_bytes_test.ts │ ├── delegate.ts │ ├── delegate_test.ts │ ├── encoding.ts │ ├── on.ts │ ├── on_test.ts │ ├── symbols.ts │ └── typed_event_target.ts ├── mod.ts ├── pipeline.ts ├── pool/ │ ├── client.ts │ ├── default_pool.ts │ ├── default_pool_test.ts │ ├── mod.ts │ └── pool.ts ├── protocol/ │ ├── deno_streams/ │ │ ├── command.ts │ │ ├── mod.ts │ │ ├── reply.ts │ │ └── reply_test.ts │ ├── shared/ │ │ ├── command.ts │ │ ├── command_test.ts │ │ ├── protocol.ts │ │ ├── reply.ts │ │ └── types.ts │ └── web_streams/ │ ├── command.ts │ ├── mod.ts │ ├── reply.ts │ └── reply_test.ts ├── redis.ts ├── stream.ts ├── subscription.ts ├── tests/ │ ├── backoff_test.ts │ ├── client_test.ts │ ├── cluster/ │ │ ├── test.ts │ │ └── test_util.ts │ ├── cluster_test.ts │ ├── commands/ │ │ ├── acl.ts │ │ ├── connection.ts │ │ ├── general.ts │ │ ├── geo.ts │ │ ├── hash.ts │ │ ├── hyper_loglog.ts │ │ ├── key.ts │ │ ├── latency.ts │ │ ├── list.ts │ │ ├── pipeline.ts │ │ ├── pubsub.ts │ │ ├── resp3.ts │ │ ├── script.ts │ │ ├── set.ts │ │ ├── sorted_set.ts │ │ ├── stream.ts │ │ └── string.ts │ ├── commands_test.ts │ ├── pool_test.ts │ ├── reconnect_test.ts │ ├── server/ │ │ └── redis.conf │ ├── test_util.ts │ └── util_test.ts └── tools/ └── format-benchmark-results.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .denov ================================================ 2.7.7 ================================================ FILE: .editorconfig ================================================ [*.ts] indent_style = space indent_size = 2 ================================================ FILE: .github/CODEOWNERS ================================================ * @uki00a ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [keroxp] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" cooldown: default-days: 1 ================================================ FILE: .github/pinact.yaml ================================================ # pinact - https://github.com/suzuki-shunsuke/pinact files: - pattern: "^\\.github/workflows/.*\\.ya?ml$" ignore_actions: ================================================ FILE: .github/workflows/build.yml ================================================ name: CI on: push: branches: - "**" pull_request: branches: - "**" jobs: test: runs-on: ubuntu-latest strategy: matrix: redis: [6.2, 7.4, 8.0] timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get Deno version run: | echo "DENO_VERSION=$(cat .denov)" >> $GITHUB_ENV - name: Set up Deno ${{ env.DENO_VERSION }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ env.DENO_VERSION }} cache: true cache-hash: ${{ hashFiles('.denov', 'deno.lock') }} - name: Set up Redis ${{ matrix.redis }} uses: shogo82148/actions-setup-redis@2f3253b148c73d7a0682eae73e862b777a4fa74e # v1.49.0 with: redis-version: ${{ matrix.redis }} auto-start: "true" - name: Run tests run: | deno task test env: REDIS_VERSION: ${{ matrix.redis }} - name: Run doc tests run: | deno task test:doc - uses: k1LoW/octocov-action@73d561f65d59e66899ed5c87e4621a913b5d5c20 # v1.5.0 if: ${{ github.event_name == 'pull_request' && matrix.redis == 8 }} lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get Deno version run: | echo "DENO_VERSION=$(cat .denov)" >> $GITHUB_ENV - name: Set up Deno ${{ env.DENO_VERSION }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ env.DENO_VERSION }} cache: true cache-hash: ${{ hashFiles('.denov', 'deno.lock') }} - name: Run linters run: | deno task check - name: deno publish --dry-run run: deno publish --dry-run benchmark: runs-on: ubuntu-latest strategy: matrix: redis: [8.0] driver: [deno-redis, ioredis] timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get Deno version run: | echo "DENO_VERSION=$(cat .denov)" >> $GITHUB_ENV - name: Set up Deno ${{ env.DENO_VERSION }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ env.DENO_VERSION }} cache: true cache-hash: ${{ hashFiles('.denov', 'deno.lock') }} - name: Set up Node.js if: matrix.driver == 'ioredis' uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: "24" - uses: bahmutov/npm-install@20216767ca67f0f7b4d095dc5859c5700a6581cb # v1.12.1 if: matrix.driver == 'ioredis' with: working-directory: benchmark - name: Set up Redis ${{ matrix.redis }} uses: shogo82148/actions-setup-redis@2f3253b148c73d7a0682eae73e862b777a4fa74e # v1.49.0 with: redis-version: ${{ matrix.redis }} auto-start: "true" - name: Run benchmarks run: | deno task bench:${{ matrix.driver }} - name: Output results as a job summary run: | deno run --allow-read=tmp tools/format-benchmark-results.js >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to JSR on: release: types: [published] workflow_dispatch: jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get Deno version run: | echo "DENO_VERSION=$(cat .denov)" >> $GITHUB_ENV - name: Set up Deno ${{ env.DENO_VERSION }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ env.DENO_VERSION }} - name: Publish packages run: deno publish ================================================ FILE: .gitignore ================================================ deno.d.ts .idea testdata/*/ benchmark/node_modules tests/tmp/*/ coverage tmp .vscode/settings.json ================================================ FILE: .octocov.yml ================================================ # generated by octocov init codeToTestRatio: code: - "**/*.ts" - "!tests/*.ts" test: - "tests/*.ts" coverage: paths: - coverage/lcov.info testExecutionTime: if: true diff: datastores: - artifact://${GITHUB_REPOSITORY} comment: # TODO: enable this if: is_pull_request # if: is_pull_request summary: if: true report: if: is_default_branch datastores: - artifact://${GITHUB_REPOSITORY} repository: denodrivers/redis ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Yusuke Sakurai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # deno-redis [![JSR](https://jsr.io/badges/@db/redis)](https://jsr.io/@db/redis) [![Build Status](https://github.com/denodrivers/redis/workflows/CI/badge.svg)](https://github.com/denodrivers/redis/actions) [![license](https://img.shields.io/github/license/denodrivers/redis.svg)](https://github.com/denodrivers/redis) [![Discord](https://img.shields.io/discord/768918486575480863?logo=discord)](https://discord.gg/QXuHBMcgWx) An experimental implementation of redis client for deno ## Usage ### Installation ```shell $ deno add jsr:@db/redis ``` ### Permissions `deno-redis` needs `--allow-net` privilege ### Stateless Commands ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1", port: 6379, }); const ok = await redis.set("hoge", "fuga"); const fuga = await redis.get("hoge"); ``` ### Pub/Sub ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); const sub = await redis.subscribe("channel"); (async function () { for await (const { channel, message } of sub.receive()) { // on message } })(); ``` ### Streams ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); await redis.xadd( "somestream", "*", // let redis assign message ID { yes: "please", no: "thankyou" }, { elements: 10 }, ); const [stream] = await redis.xread( [{ key: "somestream", xid: 0 }], // read from beginning { block: 5000 }, ); const msgFV = stream.messages[0].fieldValues; const plz = msgFV["yes"]; const thx = msgFV["no"]; ``` ### Cluster First, if you need to set up nodes into a working redis cluster: ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1", port: 6379 }); // connect each node to form a cluster (see https://redis.io/commands/cluster-meet) await redis.clusterMeet("127.0.0.1", 6380); // ... // List the nodes in the cluster await redis.clusterNodes(); // ... 127.0.0.1:6379@16379 myself,master - 0 1593978765000 0 connected // ... 127.0.0.1:6380@16380 master - 0 1593978766503 1 connected ``` To consume a redis cluster, you can use [experimental/cluster module](experimental/cluster/README.md). ## Advanced Usage ### Handle connection timeout Connection timeout handling can be implemented with `signal` option: ```ts import { connect } from "@db/redis"; using redis = await connect({ hostname: "127.0.0.1", signal: () => AbortSignal.timeout(5_000), }); ``` ### Retriable connection By default, a client's connection will retry a command execution based on exponential backoff algorithm if the server dies or the network becomes unavailable. You can change the maximum number of retries by setting `maxRetryCount` (It's default to `10`): ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1", maxRetryCount: 0 }); // Disable retries ``` ### Execute raw commands `Redis.sendCommand` is low level interface for [redis protocol](https://redis.io/topics/protocol). You can send raw redis commands and receive replies. ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); const reply = await redis.sendCommand("SET", ["redis", "nice"]); console.assert(reply === "OK"); ``` If `returnUint8Arrays` option is set to `true`, simple strings and bulk strings are returned as `Uint8Array` ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); const reply = await redis.sendCommand("GET", ["redis"], { returnUint8Arrays: true, }); console.assert(reply instanceof Uint8Array); ``` ### Pipelining https://redis.io/topics/pipelining ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); const pl = redis.pipeline(); pl.ping(); pl.ping(); pl.set("set1", "value1"); pl.set("set2", "value2"); pl.mget("set1", "set2"); pl.del("set1"); pl.del("set2"); const replies = await pl.flush(); ``` ### TxPipeline (pipeline with MULTI/EXEC) We recommend to use `tx()` instead of `multi()/exec()` for transactional operation. `MULTI/EXEC` are potentially stateful operation so that operation's atomicity is guaranteed but redis's state may change between MULTI and EXEC. `WATCH` is designed for these problems. You can ignore it by using TxPipeline because pipelined MULTI/EXEC commands are strictly executed in order at the time and no changes will happen during execution. See detail https://redis.io/topics/transactions ```ts import { connect } from "@db/redis"; const redis = await connect({ hostname: "127.0.0.1" }); const tx = redis.tx(); tx.set("a", "aa"); tx.set("b", "bb"); tx.del("c"); await tx.flush(); // MULTI // SET a aa // SET b bb // DEL c // EXEC ``` ### Client side caching https://redis.io/topics/client-side-caching ```typescript import { connect } from "@db/redis"; const mainClient = await connect({ hostname: "127.0.0.1" }); const cacheClient = await connect({ hostname: "127.0.0.1" }); const cacheClientID = await cacheClient.clientID(); await mainClient.clientTracking({ mode: "ON", redirect: cacheClientID, }); const sub = await cacheClient.subscribe("__redis__:invalidate"); (async () => { for await (const { channel, message } of sub.receive()) { // Handle invalidation messages... } })(); ``` ### Connection pooling > [!WARNING] > This feature is still experimental and may change in the future. `@db/redis/experimental/pool` module provides connection pooling: ```typescript import { createPoolClient } from "@db/redis/experimental/pool"; const redis = await createPoolClient({ connection: { hostname: "127.0.0.1", port: 6379, }, }); await redis.set("foo", "bar"); await redis.get("foo"); ``` ### Experimental features deno-redis provides some experimental features. See [experimental/README.md](experimental/README.md) for details. ## Roadmap for v1 - See https://github.com/denodrivers/redis/issues/78 ================================================ FILE: backoff.ts ================================================ export interface Backoff { /** * Returns the next backoff interval in milliseconds */ (attempts: number): number; } export interface ExponentialBackoffOptions { /** * @default 2 */ multiplier: number; /** * The maximum backoff interval in milliseconds * @default 5000 */ maxInterval: number; /** * The minimum backoff interval in milliseconds * @default 500 */ minInterval: number; } export function exponentialBackoff({ multiplier = 2, maxInterval = 5000, minInterval = 500, }: Partial = {}): Backoff { return (attempts) => Math.min(maxInterval, minInterval * (multiplier ** (attempts - 1))); } ================================================ FILE: benchmark/.npmrc ================================================ min-release-age=1 ignore-scripts=true ================================================ FILE: benchmark/benchmark.js ================================================ import { add, complete, configure, cycle, save, suite } from "benny"; import { dirname, join } from "node:path"; export function run({ driver, client, outputFilename = driver, }) { const encoder = new TextEncoder(); return suite( driver, configure({ minSamples: 10 }), add("ping", async () => { await client.ping("HELLO"); }), add("set & get", () => { const value = "bar".repeat(10); return async () => { const key = "foo"; await client.set(key, value); await client.get(key); }; }), add("set Uint8Array", () => { const value = encoder.encode("abcde".repeat(100)); return async () => { const key = "bytes"; await client.set(key, value); }; }), add("mset & mget", async () => { await client.mset({ a: "foo", b: encoder.encode("bar") }); await client.mget("a", "b"); }), add("zadd & zscore", async () => { await client.zadd("zset", 1234567, "member"); await client.zscore("zset", "member"); }), cycle(), complete((summary) => { const results = summary.results.map((result) => { const { name, ops, margin, details: { min, max, mean, median, }, samples, } = result; return { name, ops, margin, min, max, mean, median, samples, }; }); console.table(results); }), complete(async () => { await client.flushdb(); }), save({ file: outputFilename, details: true, folder: join( dirname(dirname(new URL(import.meta.url).pathname)), "tmp/benchmark", ), }), ); } ================================================ FILE: benchmark/deno-redis.ts ================================================ import { run } from "./benchmark.js"; import { connect } from "../mod.ts"; import { connect as connectWebStreams } from "../experimental/web_streams_connection/mod.ts"; { const redis = await connect({ hostname: "127.0.0.1", noDelay: true, }); try { await run({ client: redis, driver: "deno-redis", }); } finally { await redis.quit(); } } { const redis = await connectWebStreams({ hostname: "127.0.0.1" }); try { await run({ client: redis, driver: "deno-redis (experimental/web_streams_connection)", outputFilename: "deno-redis-with-experimental-web-streams-based-connection", }); } finally { await redis.quit(); } } ================================================ FILE: benchmark/ioredis.js ================================================ import { run } from "./benchmark.js"; import Redis from "ioredis"; const redis = new Redis(); redis.on("connect", async () => { await run({ client: redis, driver: "ioredis", }); redis.disconnect(); }); ================================================ FILE: benchmark/package.json ================================================ { "name": "deno-redis-benchmark", "version": "0.0.0", "type": "module", "description": "", "keywords": [], "author": "", "license": "MIT", "private": true, "devDependencies": { "benny": "^3.7.1", "ioredis": "^4.22.0" } } ================================================ FILE: client.ts ================================================ import type { Connection, SendCommandOptions } from "./connection.ts"; import type { RedisReply, RedisValue } from "./protocol/shared/types.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisSubscription, SubscribeCommand, } from "./subscription.ts"; /** * A low-level client for Redis. */ export interface Client { /** * @deprecated */ readonly connection: Connection; /** * @deprecated */ exec( command: string, ...args: RedisValue[] ): Promise; sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ): Promise; subscribe( command: SubscribeCommand, ...channelsOrPatterns: Array ): Promise>; /** * Closes a redis connection. */ close(): void; } ================================================ FILE: command.ts ================================================ import type { Binary, Bulk, BulkNil, BulkString, ConditionalArray, Integer, Protover, Raw, RedisValue, SimpleString, } from "./protocol/shared/types.ts"; import type { RedisPipeline } from "./pipeline.ts"; import type { RedisSubscription } from "./subscription.ts"; import type { StartEndCount, XAddFieldValues, XClaimOpts, XClaimReply, XId, XIdAdd, XIdInput, XIdNeg, XIdPos, XInfoConsumersReply, XInfoGroupsReply, XInfoStreamFullReply, XInfoStreamReply, XKeyId, XKeyIdGroup, XKeyIdGroupLike, XKeyIdLike, XMaxlen, XMessage, XPendingCount, XPendingReply, XReadGroupOpts, XReadOpts, XReadReply, } from "./stream.ts"; export type ACLLogMode = "RESET"; type BitopOperation = "AND" | "OR" | "XOR" | "NOT"; export interface BitfieldOpts { get?: { type: string; offset: number | string }; set?: { type: string; offset: number | string; value: number }; incrby?: { type: string; offset: number | string; increment: number }; } export interface BitfieldWithOverflowOpts extends BitfieldOpts { overflow: "WRAP" | "SAT" | "FAIL"; } export type ClientCachingMode = "YES" | "NO"; export interface ClientKillOpts { addr?: string; // ip:port laddr?: string; // ip:port id?: number; type?: ClientType; user?: string; skipme?: "YES" | "NO"; } export interface ClientListOpts { type?: ClientType; ids?: number[]; } export type ClientPauseMode = "WRITE" | "ALL"; export interface ClientTrackingOpts { mode: "ON" | "OFF"; redirect?: number; prefixes?: string[]; bcast?: boolean; optIn?: boolean; optOut?: boolean; noLoop?: boolean; } export type ClientType = "NORMAL" | "MASTER" | "REPLICA" | "PUBSUB"; export type ClientUnblockingBehaviour = "TIMEOUT" | "ERROR"; export type ClusterFailoverMode = "FORCE" | "TAKEOVER"; export type ClusterResetMode = "HARD" | "SOFT"; export type ClusterSetSlotSubcommand = | "IMPORTING" | "MIGRATING" | "NODE" | "STABLE"; export interface MigrateOpts { copy?: boolean; replace?: boolean; auth?: string; keys?: string[]; } export interface RestoreOpts { replace?: boolean; absttl?: boolean; idletime?: number; freq?: number; } export interface HelloOpts { protover: Protover; auth?: { username: string; password: string; }; clientName?: string; } export interface StralgoOpts { idx?: boolean; len?: boolean; minmatchlen?: number; withmatchlen?: boolean; } export type StralgoAlgorithm = "LCS"; export type StralgoTarget = "KEYS" | "STRINGS"; export interface SetOpts { ex?: number; px?: number; /** Sets `NX` option. */ nx?: boolean; /** Sets `XX` option. */ xx?: boolean; /** * Sets `EXAT` option. * * `EXAT` option was added in Redis v6.2. */ exat?: number; /** * Sets `PXAT` option. * * `PXAT` option was added in Redis v6.2. */ pxat?: number; keepttl?: boolean; /** * Enables `GET` option. * * `GET` option was added in Redis v6.2. */ get?: boolean; } /** Return type for {@linkcode RedisCommands.set} */ export type SetReply = T extends { get: true } ? SimpleString | BulkNil : T extends { nx: true } ? SimpleString | BulkNil : T extends { xx: true } ? SimpleString | BulkNil : T extends SetWithModeOpts ? SimpleString | BulkNil : SimpleString; /** * @deprecated Use {@linkcode SetOpts.nx}/{@linkcode SetOpts.xx} instead. This type will be removed in the future. */ export interface SetWithModeOpts extends SetOpts { /** * @deprecated Use {@linkcode SetOpts.nx}/{@linkcode SetOpts.xx} instead. This option will be removed in the future. */ mode: "NX" | "XX"; } export interface GeoRadiusOpts { withCoord?: boolean; withDist?: boolean; withHash?: boolean; count?: number; sort?: "ASC" | "DESC"; store?: string; storeDist?: string; } export type GeoUnit = "m" | "km" | "ft" | "mi"; interface BaseScanOpts { pattern?: string; count?: number; } export interface ScanOpts extends BaseScanOpts { type?: string; } export type HScanOpts = BaseScanOpts; export type SScanOpts = BaseScanOpts; export type ZScanOpts = BaseScanOpts; export interface ZAddOpts { /** @deprecated Use {@linkcode ZAddOpts.nx}/{@linkcode ZAddOpts.xx} instead. This option will be removed in the future. */ mode?: "NX" | "XX"; ch?: boolean; /** Enables `NX` option */ nx?: boolean; /** Enables `XX` option */ xx?: boolean; } /** Return type for {@linkcode RedisCommands.zadd} */ export type ZAddReply = // TODO: Uncomment the following: // T extends { mode: "NX" | "XX" } // ? Integer | BulkNil // : T extends { nx: true } ? Integer | BulkNil : T extends { xx: true } ? Integer | BulkNil : Integer; interface ZStoreOpts { aggregate?: "SUM" | "MIN" | "MAX"; } export type ZInterstoreOpts = ZStoreOpts; export type ZUnionstoreOpts = ZStoreOpts; export interface ZRangeOpts { withScore?: boolean; } export type ZInterOpts = { withScore?: boolean; } & ZStoreOpts; export interface ZRangeByLexOpts { limit?: { offset: number; count: number }; } export interface ZRangeByScoreOpts { withScore?: boolean; limit?: { offset: number; count: number }; } interface BaseLPosOpts { rank?: number; maxlen?: number; } export interface LPosOpts extends BaseLPosOpts { count?: null | undefined; } export interface LPosWithCountOpts extends BaseLPosOpts { count: number; } export type LInsertLocation = "BEFORE" | "AFTER"; export interface MemoryUsageOpts { samples?: number; } type RoleReply = | ["master", Integer, BulkString[][]] | ["slave", BulkString, Integer, BulkString, Integer] | ["sentinel", BulkString[]]; export type ScriptDebugMode = "YES" | "SYNC" | "NO"; export interface SortOpts { by?: string; limit?: { offset: number; count: number }; patterns?: string[]; order?: "ASC" | "DESC"; alpha?: boolean; } export interface SortWithDestinationOpts extends SortOpts { destination: string; } export type ShutdownMode = "NOSAVE" | "SAVE"; export interface RedisCommands { // Connection auth(password: string): Promise; auth(username: string, password: string): Promise; echo(message: RedisValue): Promise; ping(): Promise; ping(message: RedisValue): Promise; quit(): Promise; select(index: number): Promise; hello(opts?: HelloOpts): Promise; // Keys del(...keys: string[]): Promise; dump(key: string): Promise; exists(...keys: string[]): Promise; expire(key: string, seconds: number): Promise; expireat(key: string, timestamp: string): Promise; keys(pattern: string): Promise; migrate( host: string, port: number | string, key: string, destination_db: string, timeout: number, opts?: MigrateOpts, ): Promise; move(key: string, db: string): Promise; objectRefCount(key: string): Promise; objectEncoding(key: string): Promise; objectIdletime(key: string): Promise; objectFreq(key: string): Promise; objectHelp(): Promise; persist(key: string): Promise; pexpire(key: string, milliseconds: number): Promise; pexpireat(key: string, milliseconds_timestamp: number): Promise; pttl(key: string): Promise; randomkey(): Promise; rename(key: string, newkey: string): Promise; renamenx(key: string, newkey: string): Promise; restore( key: string, ttl: number, serialized_value: Binary, opts?: RestoreOpts, ): Promise; scan( cursor: number, opts?: ScanOpts, ): Promise<[BulkString, BulkString[]]>; sort( key: string, opts?: SortOpts, ): Promise; sort( key: string, opts?: SortWithDestinationOpts, ): Promise; touch(...keys: string[]): Promise; ttl(key: string): Promise; type(key: string): Promise; unlink(...keys: string[]): Promise; wait(numreplicas: number, timeout: number): Promise; // String append(key: string, value: RedisValue): Promise; bitcount(key: string): Promise; bitcount(key: string, start: number, end: number): Promise; bitfield( key: string, opts?: BitfieldOpts, ): Promise; bitfield( key: string, opts?: BitfieldWithOverflowOpts, ): Promise<(Integer | BulkNil)[]>; bitop( operation: BitopOperation, destkey: string, ...keys: string[] ): Promise; bitpos( key: string, bit: number, start?: number, end?: number, ): Promise; decr(key: string): Promise; decrby(key: string, decrement: number): Promise; get(key: string): Promise; getbit(key: string, offset: number): Promise; getrange(key: string, start: number, end: number): Promise; getset(key: string, value: RedisValue): Promise; incr(key: string): Promise; incrby(key: string, increment: number): Promise; incrbyfloat(key: string, increment: number): Promise; mget(...keys: string[]): Promise; mset(key: string, value: RedisValue): Promise; mset(...key_values: [string, RedisValue][]): Promise; mset(key_values: Record): Promise; msetnx(key: string, value: RedisValue): Promise; msetnx(...key_values: [string, RedisValue][]): Promise; msetnx(key_values: Record): Promise; psetex( key: string, milliseconds: number, value: RedisValue, ): Promise; set( key: string, value: RedisValue, opts?: TSetOpts, ): Promise>; setbit(key: string, offset: number, value: RedisValue): Promise; setex(key: string, seconds: number, value: RedisValue): Promise; setnx(key: string, value: RedisValue): Promise; setrange(key: string, offset: number, value: RedisValue): Promise; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, ): Promise; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { len: true }, ): Promise; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { idx: true }, ): Promise< [ string, //`"matches"` Array<[[number, number], [number, number]]>, string, // `"len"` Integer, ] >; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { idx: true; withmatchlen: true }, ): Promise< [ string, // `"matches"` Array<[[number, number], [number, number], number]>, string, // `"len"` Integer, ] >; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: StralgoOpts, ): Promise< Bulk | Integer | [ string, // `"matches"` Array<[[number, number], [number, number], number | undefined]>, string, // `"len"` Integer, ] >; strlen(key: string): Promise; // Geo geoadd( key: string, longitude: number, latitude: number, member: string, ): Promise; geoadd( key: string, ...lng_lat_members: [number, number, string][] ): Promise; geoadd( key: string, member_lng_lats: Record, ): Promise; geohash(key: string, ...members: string[]): Promise; geopos( key: string, ...members: string[] ): Promise | BulkNil>; geodist( key: string, member1: string, member2: string, unit?: "m" | "km" | "ft" | "mi", ): Promise; // FIXME: Return type is too conditional georadius( key: string, longitude: number, latitude: number, radius: number, unit: GeoUnit, opts?: GeoRadiusOpts, ): Promise; // FIXME: Return type is too conditional georadiusbymember( key: string, member: string, radius: number, unit: GeoUnit, opts?: GeoRadiusOpts, ): Promise; // Hash hdel(key: string, ...fields: string[]): Promise; hexists(key: string, field: string): Promise; hget(key: string, field: string): Promise; hgetall(key: string): Promise; hincrby(key: string, field: string, increment: number): Promise; hincrbyfloat( key: string, field: string, increment: number, ): Promise; hkeys(key: string): Promise; hlen(key: string): Promise; hmget(key: string, ...fields: string[]): Promise; /** * @deprecated since 4.0.0, use hset */ hmset(key: string, field: string, value: RedisValue): Promise; /** * @deprecated since 4.0.0, use hset */ hmset( key: string, ...field_values: [string, RedisValue][] ): Promise; /** * @deprecated since 4.0.0, use hset */ hmset( key: string, field_values: Record, ): Promise; hscan( key: string, cursor: number, opts?: HScanOpts, ): Promise<[BulkString, BulkString[]]>; /** * @description Sets `field` in the hash to `value`. * @see https://redis.io/commands/hset */ hset(key: string, field: string, value: RedisValue): Promise; /** * @description Sets the field-value pairs specified by `fieldValues` to the hash stored at `key`. * NOTE: Variadic form for `HSET` is supported only in Redis v4.0.0 or higher. */ hset(key: string, ...fieldValues: [string, RedisValue][]): Promise; /** * @description Sets the field-value pairs specified by `fieldValues` to the hash stored at `key`. * NOTE: Variadic form for `HSET` is supported only in Redis v4.0.0 or higher. */ hset(key: string, fieldValues: Record): Promise; hsetnx(key: string, field: string, value: RedisValue): Promise; hstrlen(key: string, field: string): Promise; hvals(key: string): Promise; // List blpop( timeout: number, ...keys: string[] ): Promise<[BulkString, BulkString] | BulkNil>; brpop( timeout: number, ...keys: string[] ): Promise<[BulkString, BulkString] | BulkNil>; brpoplpush( source: string, destination: string, timeout: number, ): Promise; lindex(key: string, index: number): Promise; linsert( key: string, loc: LInsertLocation, pivot: string, value: RedisValue, ): Promise; llen(key: string): Promise; lpop(key: string): Promise; lpop(key: string, count: number): Promise>; /** * Returns the index of the first matching element inside a list. * If no match is found, this method returns `undefined`. */ lpos( key: string, element: RedisValue, opts?: LPosOpts, ): Promise; /** * Returns the indexes of the first N matching elements inside a list. * If no match is found. this method returns an empty array. * * @param opts.count Maximum length of the indices returned by this method */ lpos( key: string, element: RedisValue, opts: LPosWithCountOpts, ): Promise; lpush(key: string, ...elements: RedisValue[]): Promise; lpushx(key: string, ...elements: RedisValue[]): Promise; lrange(key: string, start: number, stop: number): Promise; lrem(key: string, count: number, element: RedisValue): Promise; lset(key: string, index: number, element: RedisValue): Promise; ltrim(key: string, start: number, stop: number): Promise; rpop(key: string): Promise; rpoplpush(source: string, destination: string): Promise; rpush(key: string, ...elements: RedisValue[]): Promise; rpushx(key: string, ...elements: RedisValue[]): Promise; // HyperLogLog pfadd(key: string, ...elements: string[]): Promise; pfcount(...keys: string[]): Promise; pfmerge(destkey: string, ...sourcekeys: string[]): Promise; // PubSub psubscribe( ...patterns: string[] ): Promise>; pubsubChannels(pattern?: string): Promise; pubsubNumsub(...channels: string[]): Promise<(BulkString | Integer)[]>; pubsubNumpat(): Promise; publish(channel: string, message: RedisValue): Promise; subscribe( ...channels: string[] ): Promise>; // Set sadd(key: string, ...members: RedisValue[]): Promise; scard(key: string): Promise; sdiff(...keys: string[]): Promise; sdiffstore(destination: string, ...keys: string[]): Promise; sinter(...keys: string[]): Promise; sinterstore(destination: string, ...keys: string[]): Promise; sismember(key: string, member: RedisValue): Promise; smembers(key: string): Promise; smove( source: string, destination: string, member: RedisValue, ): Promise; spop(key: string): Promise; spop(key: string, count: number): Promise; srandmember(key: string): Promise; srandmember(key: string, count: number): Promise; srem(key: string, ...members: RedisValue[]): Promise; sscan( key: string, cursor: number, opts?: SScanOpts, ): Promise<[BulkString, BulkString[]]>; sunion(...keys: string[]): Promise; sunionstore(destination: string, ...keys: string[]): Promise; // Stream /** * The XACK command removes one or multiple messages * from the pending entries list (PEL) of a stream * consumer group. A message is pending, and as such * stored inside the PEL, when it was delivered to * some consumer, normally as a side effect of calling * XREADGROUP, or when a consumer took ownership of a * message calling XCLAIM. The pending message was * delivered to some consumer but the server is yet not * sure it was processed at least once. So new calls * to XREADGROUP to grab the messages history for a * consumer (for instance using an XId of 0), will * return such message. Similarly the pending message * will be listed by the XPENDING command, that * inspects the PEL. * * Once a consumer successfully processes a message, * it should call XACK so that such message does not * get processed again, and as a side effect, the PEL * entry about this message is also purged, releasing * memory from the Redis server. * * @param key the stream key * @param group the group name * @param xids the ids to acknowledge */ xack(key: string, group: string, ...xids: XIdInput[]): Promise; /** * Write a message to a stream. * * Returns bulk string reply, specifically: * The command returns the XId of the added entry. * The XId is the one auto-generated if * is passed * as XId argument, otherwise the command just returns * the same XId specified by the user during insertion. * @param key write to this stream * @param xid the XId of the entity written to the stream * @param field_values record object or map of field value pairs */ xadd( key: string, xid: XIdAdd, field_values: XAddFieldValues, ): Promise; /** * Write a message to a stream. * * Returns bulk string reply, specifically: * The command returns the XId of the added entry. * The XId is the one auto-generated if * is passed * as XId argument, otherwise the command just returns * the same XId specified by the user during insertion. * @param key write to this stream * @param xid the XId of the entity written to the stream * @param field_values record object or map of field value pairs * @param maxlen number of elements, and whether or not to use an approximate comparison */ xadd( key: string, xid: XIdAdd, field_values: XAddFieldValues, maxlen: XMaxlen, ): Promise; /** * In the context of a stream consumer group, this command changes the ownership of a pending message, so that the new owner is the * consumer specified as the command argument. * * It returns the claimed messages unless called with the JUSTIDs * option, in which case it returns only their XIds. * * This is a complex command! Read more at https://redis.io/commands/xclaim *
XCLAIM mystream mygroup Alice 3600000 1526569498055-0
1) 1) 1526569498055-0
   2) 1) "message"
      2) "orange"
* @param key the stream name * @param opts Various arguments for the command. The following are required: * GROUP: the name of the consumer group which will claim the messages * CONSUMER: the specific consumer which will claim the message * MIN-IDLE-TIME: claim messages whose idle time is greater than this number (milliseconds) * * The command has multiple options which can be omitted, however * most are mainly for internal use in order to transfer the * effects of XCLAIM or other commands to the AOF file and to * propagate the same effects to the slaves, and are unlikely to * be useful to normal users: * IDLE : 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. * 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. * RETRYCOUNT : 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. * 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. * 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. * @param xids the message XIds to claim */ xclaim( key: string, opts: XClaimOpts, ...xids: XIdInput[] ): Promise; /** * Removes the specified entries from a stream, * and returns the number of entries deleted, * that may be different from the number of * XIds passed to the command in case certain * XIds do not exist. * * @param key the stream key * @param xids ids to delete */ xdel(key: string, ...xids: XIdInput[]): Promise; /** * This command is used to create a new consumer group associated * with a stream. * *
   XGROUP CREATE test-man-000 test-group $ MKSTREAM
   OK
   
* * See https://redis.io/commands/xgroup * @param key stream key * @param groupName the name of the consumer group * @param xid The last argument is the XId of the last * item in the stream to consider already * delivered. In the above case we used the * special XId '$' (that means: the XId of the * last item in the stream). In this case * the consumers fetching data from that * consumer group will only see new elements * arriving in the stream. If instead you * want consumers to fetch the whole stream * history, use zero as the starting XId for * the consumer group * @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. */ xgroupCreate( key: string, groupName: string, xid: XIdInput | "$", mkstream?: boolean, ): Promise; /** * Delete a specific consumer from a group, leaving * the group itself intact. * *
XGROUP DELCONSUMER test-man-000 hellogroup 4
(integer) 0
* @param key stream key * @param groupName the name of the consumer group * @param consumerName the specific consumer to delete */ xgroupDelConsumer( key: string, groupName: string, consumerName: string, ): Promise; /** * Destroy a consumer group completely. The consumer * group will be destroyed even if there are active * consumers and pending messages, so make sure to * call this command only when really needed. *
XGROUP DESTROY test-man-000 test-group
(integer) 1
* @param key stream key * @param groupName the consumer group to destroy */ xgroupDestroy(key: string, groupName: string): Promise; /** A support command which displays text about the * various subcommands in XGROUP. */ xgroupHelp(): Promise; /** * Finally it possible to set the next message to deliver * using the SETID subcommand. Normally the next XId is set * when the consumer is created, as the last argument of * XGROUP CREATE. However using this form the next XId can * be modified later without deleting and creating the * consumer group again. For instance if you want the * consumers in a consumer group to re-process all the * messages in a stream, you may want to set its next ID * to 0:
XGROUP SETID mystream consumer-group-name 0
* * @param key stream key * @param groupName the consumer group * @param xid the XId to use for the next message delivered */ xgroupSetID( key: string, groupName: string, xid: XIdInput, ): Promise; xinfoStream(key: string): Promise; /** * returns the entire state of the stream, including entries, groups, consumers and PELs. This form is available since Redis 6.0. * @param key The stream key */ xinfoStreamFull(key: string, count?: number): Promise; /** * Get as output all the consumer groups associated * with the stream. * * @param key the stream key */ xinfoGroups(key: string): Promise; /** * Get the list of every consumer in a specific * consumer group. * * @param key the stream key * @param group list consumers for this group */ xinfoConsumers(key: string, group: string): Promise; /** * 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. * @param key the stream key to inspect */ xlen(key: string): Promise; /** * Complex command to obtain info on messages in the Pending Entries List. * * Outputs a summary about the pending messages in a given consumer group. * * @param key get pending messages on this stream key * @param group get pending messages for this group */ xpending( key: string, group: string, ): Promise; /** * Output more detailed info about pending messages: * * - The ID of the message. * - The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. * - The number of milliseconds that elapsed since the last time this message was delivered to this consumer. * - The number of times this message was delivered. * * If you pass the consumer argument to the command, it will efficiently filter for messages owned by that consumer. * @param key get pending messages on this stream key * @param group get pending messages for this group * @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. * @param consumer optional, filter by this consumer as owner */ xpendingCount( key: string, group: string, startEndCount: StartEndCount, consumer?: string, ): Promise; /** * The command returns the stream entries matching a given * range of XIds. The range is specified by a minimum and * maximum ID. All the entries having an XId between the * two specified or exactly one of the two XIds specified * (closed interval) are returned. * * The command also has a reciprocal command returning * items in the reverse order, called XREVRANGE, which * is otherwise identical. * * The - and + special XIds mean respectively the minimum * XId possible and the maximum XId possible inside a stream, * so the following command will just return every * entry in the stream.
XRANGE somestream - +
* @param key stream key * @param start beginning XId, or - * @param end final XId, or + * @param count max number of entries to return */ xrange( key: string, start: XIdNeg, end: XIdPos, count?: number, ): Promise; /** * This command is exactly like XRANGE, but with the * notable difference of returning the entries in * reverse order, and also taking the start-end range * in reverse order: in XREVRANGE you need to state the * end XId and later the start ID, and the command will * produce all the element between (or exactly like) * the two XIds, starting from the end side. * * @param key the stream key * @param start reading backwards, start from this XId. for the maximum, specify "+" * @param end stop at this XId. for the minimum, specify "-" * @param count max number of entries to return */ xrevrange( key: string, start: XIdPos, end: XIdNeg, count?: number, ): Promise; /** * Read data from one or multiple streams, only returning * entries with an XId greater than the last received XId * reported by the caller. * @param key_xids pairs containing the stream key, and * the XId from which to read * @param opts optional max count of entries to return * for each stream, and number of * milliseconds for which to block */ xread( key_xids: (XKeyId | XKeyIdLike)[], opts?: XReadOpts, ): Promise; /** * The XREADGROUP command is a special version of the XREAD command with support for consumer groups. * * @param key_ids { key, id } pairs to read * @param opts you must specify group name and consumer name. * those must be created using the XGROUP command, * prior to invoking this command. you may optionally * include a count of records to read, and the number * of milliseconds to block */ xreadgroup( key_xids: (XKeyIdGroup | XKeyIdGroupLike)[], opts: XReadGroupOpts, ): Promise; /** * Trims the stream to the indicated number * of elements.
XTRIM mystream MAXLEN 1000
* @param key * @param maxlen */ xtrim(key: string, maxlen: XMaxlen): Promise; // SortedSet bzpopmin( timeout: number, ...keys: string[] ): Promise<[BulkString, BulkString, BulkString] | BulkNil>; bzpopmax( timeout: number, ...keys: string[] ): Promise<[BulkString, BulkString, BulkString] | BulkNil>; zadd( key: string, score: number, member: RedisValue, opts?: TZAddOpts, ): Promise>; zadd( key: string, score_members: [number, RedisValue][], opts?: TZAddOpts, ): Promise>; zadd( key: string, member_scores: Record, opts?: TZAddOpts, ): Promise>; zaddIncr( key: string, score: number, member: RedisValue, opts?: ZAddOpts, ): Promise; zcard(key: string): Promise; zcount(key: string, min: number, max: number): Promise; zincrby( key: string, increment: number, member: RedisValue, ): Promise; zinter(keys: string[], opts?: ZInterOpts): Promise; zinter(key_weights: [string, number][], opts?: ZInterOpts): Promise; zinter( key_weights: Record, opts?: ZInterOpts, ): Promise; zinterstore( destination: string, keys: string[], opts?: ZInterstoreOpts, ): Promise; zinterstore( destination: string, key_weights: [string, number][], opts?: ZInterstoreOpts, ): Promise; zinterstore( destination: string, key_weights: Record, opts?: ZInterstoreOpts, ): Promise; zlexcount(key: string, min: string, max: string): Promise; zpopmax(key: string, count?: number): Promise; zpopmin(key: string, count?: number): Promise; zrange( key: string, start: number, stop: number, opts?: ZRangeOpts, ): Promise; zrangebylex( key: string, min: string, max: string, opts?: ZRangeByLexOpts, ): Promise; zrangebyscore( key: string, min: number | string, max: number | string, opts?: ZRangeByScoreOpts, ): Promise; zrank(key: string, member: RedisValue): Promise; zrem(key: string, ...members: RedisValue[]): Promise; zremrangebylex(key: string, min: string, max: string): Promise; zremrangebyrank(key: string, start: number, stop: number): Promise; zremrangebyscore( key: string, min: number | string, max: number | string, ): Promise; zrevrange( key: string, start: number, stop: number, opts?: ZRangeOpts, ): Promise; zrevrangebylex( key: string, max: string, min: string, opts?: ZRangeByLexOpts, ): Promise; zrevrangebyscore( key: string, max: number | string, min: number | string, opts?: ZRangeByScoreOpts, ): Promise; zrevrank(key: string, member: RedisValue): Promise; zscan( key: string, cursor: number, opts?: ZScanOpts, ): Promise<[BulkString, BulkString[]]>; zscore(key: string, member: RedisValue): Promise; zunionstore( destination: string, keys: string[], opts?: ZUnionstoreOpts, ): Promise; zunionstore( destination: string, key_weights: [string, number][], opts?: ZUnionstoreOpts, ): Promise; zunionstore( destination: string, key_weights: Record, opts?: ZUnionstoreOpts, ): Promise; // Client /** * This command controls the tracking of the keys in the next command executed by the connection. * @see https://redis.io/commands/client-caching */ clientCaching(mode: ClientCachingMode): Promise; /** * Returns the name of the current connection which can be set by `clientSetName`. * @see https://redis.io/commands/client-getname */ clientGetName(): Promise; /** * Returns the client ID we are redirecting our tracking notifications to. * @see https://redis.io/commands/client-getredir */ clientGetRedir(): Promise; /** * Returns the id of the current redis connection. */ clientID(): Promise; /** * Returns information and statistics about the current client connection in a mostly human readable format. * @see https://redis.io/commands/client-info */ clientInfo(): Promise; /** * Closes a given client connection. * @see https://redis.io/commands/client-kill */ clientKill(opts?: ClientKillOpts): Promise; /** * Returns information and statistics about the client connections server in a mostly human readable format. * @see https://redis.io/commands/client-list */ clientList(opts?: ClientListOpts): Promise; /** * Suspend all the Redis clients for the specified amount of time (in milliseconds). * @see https://redis.io/commands/client-pause */ clientPause(timeout: number, mode?: ClientPauseMode): Promise; /** * Sets a `connectionName` to the current connection. * You can get the name of the current connection using `clientGetName()`. * @see https://redis.io/commands/client-setname */ clientSetName(connectionName: string): Promise; /** * Enables the tracking feature for the Redis server that is used for server assisted client side caching. * @see https://redis.io/commands/client-tracking */ clientTracking(opts: ClientTrackingOpts): Promise; /** * Returns information about the current client connection's use of the server assisted client side caching feature. * @see https://redis.io/commands/client-trackinginfo */ clientTrackingInfo(): Promise; /** * This command can unblock, from a different connection, a client blocked in a blocking operation. * @see https://redis.io/commands/client-unblock */ clientUnblock( id: number, behaviour?: ClientUnblockingBehaviour, ): Promise; /** * Used to resume command processing for all clients that were paused by `clientPause`. * @see https://redis.io/commands/client-unpause */ clientUnpause(): Promise; // Cluster /** * @see https://redis.io/topics/cluster-spec */ asking(): Promise; clusterAddSlots(...slots: number[]): Promise; clusterCountFailureReports(node_id: string): Promise; clusterCountKeysInSlot(slot: number): Promise; clusterDelSlots(...slots: number[]): Promise; clusterFailover(mode?: ClusterFailoverMode): Promise; clusterFlushSlots(): Promise; clusterForget(node_id: string): Promise; clusterGetKeysInSlot(slot: number, count: number): Promise; clusterInfo(): Promise; clusterKeySlot(key: string): Promise; clusterMeet(ip: string, port: number): Promise; clusterMyID(): Promise; clusterNodes(): Promise; clusterReplicas(node_id: string): Promise; clusterReplicate(node_id: string): Promise; clusterReset(mode?: ClusterResetMode): Promise; clusterSaveConfig(): Promise; clusterSetSlot( slot: number, subcommand: ClusterSetSlotSubcommand, node_id?: string, ): Promise; clusterSlaves(node_id: string): Promise; clusterSlots(): Promise; readonly(): Promise; readwrite(): Promise; // Server aclCat(categoryname?: string): Promise; aclDelUser(...usernames: string[]): Promise; aclGenPass(bits?: number): Promise; aclGetUser(username: string): Promise<(BulkString | BulkString[])[]>; aclHelp(): Promise; aclList(): Promise; aclLoad(): Promise; aclLog(count: number): Promise; aclLog(mode: ACLLogMode): Promise; aclSave(): Promise; aclSetUser(username: string, ...rules: string[]): Promise; aclUsers(): Promise; aclWhoami(): Promise; bgrewriteaof(): Promise; bgsave(): Promise; command(): Promise< [BulkString, Integer, BulkString[], Integer, Integer, Integer][] >; commandCount(): Promise; commandGetKeys(): Promise; commandInfo( ...command_names: string[] ): Promise< ([BulkString, Integer, BulkString[], Integer, Integer, Integer] | BulkNil)[] >; configGet(parameter: string): Promise; configResetStat(): Promise; configRewrite(): Promise; configSet(parameter: string, value: string): Promise; dbsize(): Promise; debugObject(key: string): Promise; debugSegfault(): Promise; flushall(async?: boolean): Promise; flushdb(async?: boolean): Promise; info(section?: string): Promise; lastsave(): Promise; memoryDoctor(): Promise; memoryHelp(): Promise; memoryMallocStats(): Promise; memoryPurge(): Promise; memoryStats(): Promise; memoryUsage(key: string, opts?: MemoryUsageOpts): Promise; moduleList(): Promise; moduleLoad(path: string, ...args: string[]): Promise; moduleUnload(name: string): Promise; monitor(): void; replicaof(host: string, port: number): Promise; replicaofNoOne(): Promise; role(): Promise; save(): Promise; shutdown(mode?: ShutdownMode): Promise; slaveof(host: string, port: number): Promise; slaveofNoOne(): Promise; slowlog(subcommand: string, ...args: string[]): Promise; swapdb(index1: number, index2: number): Promise; sync(): void; time(): Promise<[BulkString, BulkString]>; // Scripting eval(script: string, keys: string[], args: RedisValue[]): Promise; evalsha(sha1: string, keys: string[], args: RedisValue[]): Promise; scriptDebug(mode: ScriptDebugMode): Promise; scriptExists(...sha1s: string[]): Promise; scriptFlush(): Promise; scriptKill(): Promise; scriptLoad(script: string): Promise; // Transactions discard(): Promise; exec(): Promise; multi(): Promise; unwatch(): Promise; watch(...keys: string[]): Promise; // Latency latencyDoctor(): Promise; // Pipeline tx(): RedisPipeline; pipeline(): RedisPipeline; } ================================================ FILE: connection.ts ================================================ import type { Backoff } from "./backoff.ts"; import type { ConnectionEventMap } from "./events.ts"; import type { ErrorReplyError } from "./errors.ts"; import type { TypedEventTarget } from "./internal/typed_event_target.ts"; import type { kUnstableCreateProtocol, kUnstablePipeline, kUnstableProtover, kUnstableReadReply, kUnstableStartReadLoop, kUnstableWriteCommand, } from "./internal/symbols.ts"; import type { Command, Protocol } from "./protocol/shared/protocol.ts"; import type { Protover, RedisReply, RedisValue, } from "./protocol/shared/types.ts"; export interface SendCommandOptions { /** * When this option is set, simple or bulk string replies are returned as `Uint8Array` type. * * @default false */ returnUint8Arrays?: boolean; } export interface Connection extends TypedEventTarget { /** @deprecated */ name: string | null; isClosed: boolean; isConnected: boolean; close(): void; [Symbol.dispose](): void; connect(): Promise; reconnect(): Promise; sendCommand( command: string, args?: Array, options?: SendCommandOptions, ): Promise; /** * @private */ [kUnstableReadReply](returnsUint8Arrays?: boolean): Promise; /** * @private */ [kUnstableWriteCommand](command: Command): Promise; /** * @private */ [kUnstablePipeline]( commands: Array, ): Promise>; /** * @private */ [kUnstableStartReadLoop]( binaryMode?: boolean, ): AsyncIterableIterator; } export interface RedisConnectionOptions { tls?: boolean; /** * A list of root certificates, implies {@linkcode RedisConnectionOptions.tls} */ caCerts?: string[]; db?: number; password?: string; username?: string; name?: string; /** * @default 10 */ maxRetryCount?: number; backoff?: Backoff; /** * When this option is set, a `PING` command is sent every specified number of seconds. */ healthCheckInterval?: number; /** * 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. * * Works only in Deno v2.3 or later. */ signal?: () => AbortSignal; /** * If `true`, disables Nagle's algorithm. * * @default false */ noDelay?: boolean; /** * @private */ [kUnstableCreateProtocol]?: (conn: Deno.Conn) => Protocol; /** * @private */ [kUnstableProtover]?: Protover; } ================================================ FILE: default_client.ts ================================================ import type { Client } from "./client.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisSubscription, SubscribeCommand, } from "./subscription.ts"; import type { Connection, SendCommandOptions } from "./connection.ts"; import { DefaultRedisSubscription } from "./default_subscription.ts"; import type { RedisReply, RedisValue } from "./protocol/shared/types.ts"; export function createDefaultClient(connection: Connection): Client { return new DefaultClient(connection); } class DefaultClient implements Client { constructor(readonly connection: Connection) {} exec( command: string, ...args: RedisValue[] ): Promise { return this.connection.sendCommand(command, args); } sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ) { return this.connection.sendCommand(command, args, options); } async subscribe< TMessage extends PubSubMessageType = DefaultPubSubMessageType, >( command: SubscribeCommand, ...channelsOrPatterns: Array ): Promise> { const subscription = new DefaultRedisSubscription( this.connection, ); switch (command) { case "SUBSCRIBE": await subscription.subscribe(...channelsOrPatterns); break; case "PSUBSCRIBE": await subscription.psubscribe(...channelsOrPatterns); break; } return subscription; } close(): void { this.connection.close(); } } ================================================ FILE: default_connection.ts ================================================ import type { Backoff } from "./backoff.ts"; import { exponentialBackoff } from "./backoff.ts"; import type { Connection, RedisConnectionOptions, SendCommandOptions, } from "./connection.ts"; import { ErrorReplyError, InvalidStateError, isRetriableError, } from "./errors.ts"; import type { ConnectionEventMap } from "./events.ts"; import { kUnstableCreateProtocol, kUnstablePipeline, kUnstableProtover, kUnstableReadReply, kUnstableStartReadLoop, kUnstableWriteCommand, } from "./internal/symbols.ts"; import type { TypedEventTarget } from "./internal/typed_event_target.ts"; import { createTypedEventTarget, dispatchEvent, } from "./internal/typed_event_target.ts"; import { kEmptyRedisArgs } from "./protocol/shared/command.ts"; import type { Command, Protocol } from "./protocol/shared/protocol.ts"; import { Protocol as DenoStreamsProtocol } from "./protocol/deno_streams/mod.ts"; import type { RedisReply, RedisValue } from "./protocol/shared/types.ts"; import { delay } from "./deps/std/async.ts"; export function createRedisConnection( hostname: string, port: number | string | undefined, options: RedisConnectionOptions, ): Connection { return new RedisConnection(hostname, port ?? 6379, options); } interface PendingCommand { execute: () => Promise; resolve: (reply: RedisReply) => void; reject: (error: unknown) => void; } class RedisConnection implements Connection, TypedEventTarget { name: string | null = null; private maxRetryCount = 10; private readonly hostname: string; private readonly port: number | string; private _isClosed = false; private _isConnected = false; private backoff: Backoff; private commandQueue: PendingCommand[] = []; #conn!: Deno.Conn; #protocol!: Protocol; #eventTarget = createTypedEventTarget(); #connectingPromise?: PromiseWithResolvers; get isClosed(): boolean { return this._isClosed; } get isConnected(): boolean { return this._isConnected; } get isRetriable(): boolean { return this.maxRetryCount > 0; } constructor( hostname: string, port: number | string, private options: RedisConnectionOptions, ) { this.hostname = hostname; this.port = port; if (options.name) { this.name = options.name; } if (options.maxRetryCount != null) { this.maxRetryCount = options.maxRetryCount; } this.backoff = options.backoff ?? exponentialBackoff(); } private async authenticate( username: string | undefined, password: string, ): Promise { try { // TODO: Use `HELLO` instead of `AUTH` password && username ? await this.#sendCommandImmediately("AUTH", [username, password]) : await this.#sendCommandImmediately("AUTH", [password]); } catch (error) { if (error instanceof ErrorReplyError) { const authError = new AuthenticationError("Authentication failed", { cause: error, }); dispatchEvent(this.#eventTarget, "error", { error: authError }); throw authError; } else { dispatchEvent(this.#eventTarget, "error", { error }); throw error; } } } private async selectDb( db: number | undefined = this.options.db, ): Promise { if (!db) throw new Error("The database index is undefined."); await this.#sendCommandImmediately("SELECT", [db]); } private enqueueCommand( command: PendingCommand, ) { this.commandQueue.push(command); if (!this.#isProcessingQueuedCommands) { this.#isProcessingQueuedCommands = true; this.processCommandQueue(); } } sendCommand( command: string, args?: Array, options?: SendCommandOptions, ): Promise { const execute = () => this.#protocol.sendCommand( command, args ?? kEmptyRedisArgs, options?.returnUint8Arrays, ); const { promise, resolve, reject } = Promise.withResolvers(); this.enqueueCommand({ execute, resolve, reject }); return promise; } /** * Executes a command immediately, bypassing the queue. */ #sendCommandImmediately( command: string, args?: Array, ): Promise { const isConnecting = this.#connectingPromise != null; if (!isConnecting) { return Promise.reject( new InvalidStateError( `Unexpected inline command execution detected (command: ${command})`, ), ); } return this.#protocol.sendCommand( command, args ?? kEmptyRedisArgs, ); } addEventListener( type: K, callback: (event: CustomEvent) => void, options?: AddEventListenerOptions | boolean, ): void { return this.#eventTarget.addEventListener( type, callback as (event: Event) => void, options, ); } removeEventListener( type: K, callback: (event: CustomEvent) => void, options?: EventListenerOptions | boolean, ): void { return this.#eventTarget.removeEventListener( type, callback as (event: Event) => void, options, ); } [kUnstableReadReply](returnsUint8Arrays?: boolean): Promise { return this.#protocol.readReply(returnsUint8Arrays); } [kUnstablePipeline](commands: Array): Promise { const { promise, resolve, reject } = Promise.withResolvers< RedisReply[] >(); const execute = () => this.#protocol.pipeline(commands); this.enqueueCommand({ execute, resolve, reject } as PendingCommand); return promise; } [kUnstableWriteCommand](command: Command): Promise { return this.#protocol.writeCommand(command); } async *[kUnstableStartReadLoop]( binaryMode?: boolean, ): AsyncIterableIterator { let forceReconnect = false; while (this.isConnected) { try { let rep: RedisReply; try { rep = await this[kUnstableReadReply](binaryMode); } catch (err) { if (this.isClosed) { // Connection already closed by the user. break; } throw err; // Connection may have been unintentionally closed. } yield rep; } catch (error) { if (isRetriableError(error)) { forceReconnect = true; } else throw error; } finally { if ((!this.isClosed && !this.isConnected) || forceReconnect) { forceReconnect = false; await this.reconnect(); } } } } /** * Connect to Redis server */ connect(): Promise { if (this.#connectingPromise) { return this.#connectingPromise.promise; } const promiseWithResolvers = Promise.withResolvers(); this.#connectingPromise = promiseWithResolvers; (async () => { try { await this.#connect(0); promiseWithResolvers.resolve(); this.#connectingPromise = undefined; } catch (error) { promiseWithResolvers.reject(error); this.#connectingPromise = undefined; } })(); return promiseWithResolvers.promise; } async #connect(retryCount: number) { try { const signal: AbortSignal | undefined = this.options?.signal?.() ?? undefined; const dialOpts: Deno.ConnectOptions = { hostname: this.hostname, port: parsePortLike(this.port), signal, }; const conn = this.options?.tls || this.options?.caCerts != null ? await Deno.connectTls({ ...dialOpts, caCerts: this.options?.caCerts, }) : await Deno.connect(dialOpts); if (this.options?.noDelay && "setNoDelay" in conn) { conn.setNoDelay(); } this.#conn = conn; this.#protocol = this.options?.[kUnstableCreateProtocol]?.(conn) ?? new DenoStreamsProtocol(conn); this._isClosed = false; this._isConnected = true; dispatchEvent(this.#eventTarget, "connect", undefined); try { if (this.options.password != null) { await this.authenticate(this.options.username, this.options.password); } if (this.options[kUnstableProtover] != null) { await this.#sendCommandImmediately("HELLO", [ this.options[kUnstableProtover], ]); } if (this.options.db) { await this.selectDb(this.options.db); } } catch (error) { this.#close(); throw error; } dispatchEvent(this.#eventTarget, "ready", undefined); this.#enableHealthCheckIfNeeded(); } catch (error) { if (error instanceof AuthenticationError) { dispatchEvent(this.#eventTarget, "error", { error }); dispatchEvent(this.#eventTarget, "end", undefined); throw (error.cause ?? error); } const backoff = this.backoff(retryCount); retryCount++; if (retryCount >= this.maxRetryCount) { dispatchEvent(this.#eventTarget, "error", { error: error as Error }); dispatchEvent(this.#eventTarget, "end", undefined); throw error; } dispatchEvent(this.#eventTarget, "reconnecting", { delay: backoff }); await delay(backoff); await this.#connect(retryCount); } } close() { return this[Symbol.dispose](); } [Symbol.dispose](): void { return this.#close(false); } #close(canReconnect = false) { const isClosedAlready = this._isClosed; this._isClosed = true; this._isConnected = false; try { this.#conn!.close(); } catch (error) { if (!(error instanceof Deno.errors.BadResource)) { dispatchEvent(this.#eventTarget, "error", { error: error as Error }); throw error; } } finally { if (!isClosedAlready) { dispatchEvent(this.#eventTarget, "close", undefined); if (!canReconnect) { dispatchEvent(this.#eventTarget, "end", undefined); } } } } async reconnect(): Promise { try { await this.sendCommand("PING"); this._isConnected = true; } catch (error) { dispatchEvent(this.#eventTarget, "error", { error }); this.#close(true); await this.connect(); await this.sendCommand("PING"); } } #isProcessingQueuedCommands = false; private async processCommandQueue() { const [command] = this.commandQueue; if (!command) { this.#isProcessingQueuedCommands = false; return; } try { const reply = await command.execute(); command.resolve(reply); } catch (error) { if ( !isRetriableError(error) || this.isManuallyClosedByUser() ) { dispatchEvent(this.#eventTarget, "error", { error }); return command.reject(error); } let backoff = 0; for (let i = 0; i < this.maxRetryCount; i++) { // Try to reconnect to the server and retry the command this.#close(true); try { dispatchEvent(this.#eventTarget, "reconnecting", { delay: backoff }); await this.connect(); const reply = await command.execute(); return command.resolve(reply); } catch (error) { dispatchEvent(this.#eventTarget, "error", { error }); // TODO: use `AggregateError`? backoff = this.backoff(i); await delay(backoff); } } dispatchEvent(this.#eventTarget, "error", { error }); command.reject(error); } finally { this.commandQueue.shift(); this.processCommandQueue(); } } private isManuallyClosedByUser(): boolean { return this._isClosed && !this._isConnected; } #enableHealthCheckIfNeeded() { const { healthCheckInterval } = this.options; if (healthCheckInterval == null) { return; } const ping = async () => { if (this.isManuallyClosedByUser()) { return; } try { await this.sendCommand("PING"); this._isConnected = true; } catch (_error) { // TODO: notify the user of an error this._isConnected = false; } finally { setTimeout(ping, healthCheckInterval); } }; setTimeout(ping, healthCheckInterval); } } class AuthenticationError extends Error {} function parsePortLike(port: string | number | undefined): number { let parsedPort: number; if (typeof port === "string") { parsedPort = parseInt(port); } else if (typeof port === "number") { parsedPort = port; } else { parsedPort = 6379; } if (!Number.isSafeInteger(parsedPort)) { throw new Error("Port is invalid"); } return parsedPort; } ================================================ FILE: default_subscription.ts ================================================ import { decoder } from "./internal/encoding.ts"; import { kUnstableStartReadLoop, kUnstableWriteCommand, } from "./internal/symbols.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisPubSubMessage, RedisSubscription, } from "./subscription.ts"; import type { Connection } from "./connection.ts"; import type { Binary } from "./protocol/shared/types.ts"; export class DefaultRedisSubscription< TMessage extends PubSubMessageType = DefaultPubSubMessageType, > implements RedisSubscription { get isConnected(): boolean { return this.connection.isConnected; } get isClosed(): boolean { return this.connection.isClosed; } private channels = Object.create(null); private patterns = Object.create(null); constructor(private readonly connection: Connection) {} async psubscribe(...patterns: string[]) { await this.#writeCommand("PSUBSCRIBE", patterns); for (const pat of patterns) { this.patterns[pat] = true; } } async punsubscribe(...patterns: string[]) { await this.#writeCommand("PUNSUBSCRIBE", patterns); for (const pat of patterns) { delete this.patterns[pat]; } } async subscribe(...channels: string[]) { await this.#writeCommand("SUBSCRIBE", channels); for (const chan of channels) { this.channels[chan] = true; } } async unsubscribe(...channels: string[]) { await this.#writeCommand("UNSUBSCRIBE", channels); for (const chan of channels) { delete this.channels[chan]; } } receive(): AsyncIterableIterator> { return this.#receive(false); } receiveBuffers(): AsyncIterableIterator> { return this.#receive(true); } async *#receive< T = TMessage, >( binaryMode: boolean, ): AsyncIterableIterator< RedisPubSubMessage > { const onConnectionRecovered = async () => { if (Object.keys(this.channels).length > 0) { await this.subscribe(...Object.keys(this.channels)); } if (Object.keys(this.patterns).length > 0) { await this.psubscribe(...Object.keys(this.patterns)); } }; this.connection.addEventListener("connect", onConnectionRecovered); const iter = this.connection[kUnstableStartReadLoop](binaryMode); try { for await (const _rep of iter) { const rep = _rep as ([string | Binary, string | Binary, T] | [ string | Binary, string | Binary, string | Binary, T, ]); const event = rep[0] instanceof Uint8Array ? decoder.decode(rep[0]) : rep[0]; if (event === "message" && rep.length === 3) { const channel = rep[1] instanceof Uint8Array ? decoder.decode(rep[1]) : rep[1]; const message = rep[2]; yield { channel, message, }; } else if (event === "pmessage" && rep.length === 4) { const pattern = rep[1] instanceof Uint8Array ? decoder.decode(rep[1]) : rep[1]; const channel = rep[2] instanceof Uint8Array ? decoder.decode(rep[2]) : rep[2]; const message = rep[3]; yield { pattern, channel, message, }; } } } finally { this.connection.removeEventListener( "connect", onConnectionRecovered, ); } } close() { this.connection.close(); } async #writeCommand(command: string, args: Array): Promise { await this.connection[kUnstableWriteCommand]({ command, args }); } } ================================================ FILE: deno.json ================================================ { "name": "@db/redis", "version": "0.41.0", "exports": { ".": "./mod.ts", "./experimental/pool": "./experimental/pool/mod.ts", "./experimental/cluster": "./experimental/cluster/mod.ts", "./experimental/web-streams-connection": "./experimental/web_streams_connection/mod.ts" }, "exclude": [ "benchmark/node_modules", "tmp" ], "lint": { "exclude": [ "benchmark/benchmark.js", "benchmark/ioredis.js" ], "plugins": ["jsr:@uki00a/deno-lint-plugin-extra-rules@0.9.0"], "rules": { "include": ["no-console"] } }, "test": { "exclude": ["benchmark/", "vendor/"], "permissions": { "net": ["127.0.0.1"], "read": ["tests"], "write": ["tests/tmp"], "run": ["redis-server", "redis-cli"], "env": ["REDIS_VERSION"] } }, "permissions": { "bench:deno-redis": { "net": ["127.0.0.1:6379"], "env": [ "NODE_DISABLE_COLORS", "TERM", "TERM_PROGRAM", "GRACEFUL_FS_PLATFORM", "TEST_GRACEFUL_FS_GLOBAL_PATCH" ], "write": ["tmp"] } }, "publish": { "exclude": [ ".denov", ".editorconfig", ".octocov.yml", ".github", "import_map.dev.json", "import_map.test.json", "benchmark/", "tests/", "**/*_test.ts", "tools/" ] }, "minimumDependencyAge": 1440, "tasks": { "lock": "deno cache --reload --import-map=import_map.dev.json mod.ts experimental/**/mod.ts tests/**/*.ts benchmark/*.ts", "check": { "dependencies": ["check:lint", "check:deno.json", "typecheck"] }, "check:lint": "deno lint && deno fmt --check", "check:deno.json": "deno run -R=deno.json --import-map=import_map.dev.json @uki00a/deno-json-lint", "typecheck": "cat deno.json | jq '.exports.[]' | xargs deno check", "test": "DENO_FUTURE=1 deno test --permission-set --coverage=coverage --trace-leaks --frozen-lockfile", "test:doc": "deno check --doc-only --import-map=import_map.test.json README.md experimental/cluster/README.md", "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", "bench:ioredis": "node benchmark/ioredis.js" }, "deno-json-lint": { "rules": { "no-restricted-fields": ["error", { "fields": { "imports": "Use `import_map.dev.json` instead." } }] } } } ================================================ FILE: deps/cluster-key-slot.js ================================================ export { default as calculateSlot } from "npm:cluster-key-slot@1.1.0"; ================================================ FILE: deps/std/assert.ts ================================================ export * from "jsr:@std/assert@^1"; ================================================ FILE: deps/std/async.ts ================================================ export * from "jsr:@std/async@^1/delay"; ================================================ FILE: deps/std/bytes.ts ================================================ export * from "jsr:@std/bytes@^1/concat"; ================================================ FILE: deps/std/collections.ts ================================================ export { distinctBy } from "jsr:@std/collections@^1/distinct-by"; ================================================ FILE: deps/std/io.ts ================================================ export { BufReader } from "jsr:@std/io@0.224.5/buf-reader"; export { BufWriter } from "jsr:@std/io@0.224.5/buf-writer"; export { readerFromStreamReader } from "jsr:@std/io@0.224.5/reader-from-stream-reader"; export { readAll } from "jsr:@std/io@0.224.5/read-all"; ================================================ FILE: deps/std/random.ts ================================================ export { sample } from "jsr:@std/random@0.1.0/sample"; export { shuffle } from "jsr:@std/random@0.1.0/shuffle"; ================================================ FILE: deps/std/testing.ts ================================================ export * from "jsr:@std/testing@^1/bdd"; export type * from "jsr:@std/testing@^1/types"; export { assertType } from "jsr:@std/testing@^1/types"; ================================================ FILE: errors.ts ================================================ export class EOFError extends Error {} export class ConnectionClosedError extends Error {} export class SubscriptionClosedError extends Error {} export class ErrorReplyError extends Error {} export class NotImplementedError extends Error { constructor(message?: string) { super(message ? `Not implemented: ${message}` : "Not implemented"); } } export class InvalidStateError extends Error { constructor(message?: string) { const base = "Invalid state"; super(message ? `${base}: ${message}` : base); } } export function isRetriableError(error: unknown): boolean { return (error instanceof Deno.errors.BadResource || error instanceof Deno.errors.BrokenPipe || error instanceof Deno.errors.ConnectionAborted || error instanceof Deno.errors.ConnectionRefused || error instanceof Deno.errors.ConnectionReset || error instanceof Deno.errors.UnexpectedEof || error instanceof EOFError); } ================================================ FILE: events.ts ================================================ export type ConnectionEvent = Record; export type ConnectionErrorEventDetails = { error: unknown; }; export type ConnectionReconnectingEventDetails = { delay: number; }; export type ConnectionEventMap = { error: ConnectionErrorEventDetails; connect: unknown; reconnecting: ConnectionReconnectingEventDetails; ready: unknown; close: unknown; end: unknown; }; export type ConnectionEventType = | "error" | "connect" | "reconnecting" | "ready" | "close" | "end"; ================================================ FILE: executor.ts ================================================ import type { Client } from "./client.ts"; /** @deprecated Use {@linkcode Client} instead. */ export type CommandExecutor = Client; ================================================ FILE: experimental/README.md ================================================ # Experimental features deno-redis has some experimental features: - [Redis Cluster client](cluster/README.md) **These experimental features may be subject to breaking changes even after a major release.** ================================================ FILE: experimental/cluster/README.md ================================================ # experimental/cluster [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/redis/experimental/cluster/mod.ts) This module provides a client impelementation for [the Redis Cluster](https://redis.io/topics/cluster-tutorial). The implementation is based on the [antirez/redis-rb-cluster](https://github.com/antirez/redis-rb-cluster). ## Usage ```typescript import { connect } from "@db/redis/experimental/cluster"; const cluster = await connect({ nodes: [ { hostname: "127.0.0.1", port: 7000, }, { hostname: "127.0.0.1", port: 7001, }, ], }); await cluster.get("{foo}bar"); ``` ================================================ FILE: experimental/cluster/mod.ts ================================================ /** * @module * @experimental **NOTE**: This is an unstable module. * * Based on https://github.com/antirez/redis-rb-cluster which is licensed as follows: * * Copyright (C) 2013 Salvatore Sanfilippo * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { connect, create } from "../../redis.ts"; import type { RedisConnectOptions } from "../../redis.ts"; import type { Client } from "../../client.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisSubscription, SubscribeCommand, } from "../../subscription.ts"; import type { Connection, SendCommandOptions } from "../../connection.ts"; import type { Redis } from "../../redis.ts"; import type { RedisReply, RedisValue } from "../../protocol/shared/types.ts"; import { ErrorReplyError, NotImplementedError } from "../../errors.ts"; import { delay } from "../../deps/std/async.ts"; import { distinctBy } from "../../deps/std/collections.ts"; import { sample, shuffle } from "../../deps/std/random.ts"; import { calculateSlot } from "../../deps/cluster-key-slot.js"; export interface ClusterConnectOptions { nodes: Array; maxConnections?: number; newRedis?: (opts: RedisConnectOptions) => Promise; } export interface NodeOptions { hostname: string; port?: number; } interface SlotMap { [slot: number]: ClusterNode; } class ClusterNode { readonly name: string; constructor(readonly hostname: string, readonly port: number) { this.name = `${hostname}:${port}`; } static parseIPAndPort(ipAndPort: string): ClusterNode { const [ip, port] = ipAndPort.split(":"); const node = new ClusterNode( ip, parseInt(port), ); return node; } } const kRedisClusterRequestTTL = 16; class ClusterError extends Error {} class ClusterClient implements Client { #nodeBySlot!: SlotMap; #startupNodes: ClusterNode[]; #refreshTableASAP?: boolean; #maxConnections: number; #connectionByNodeName: { [name: string]: Redis } = {}; #newRedis: (opts: RedisConnectOptions) => Promise; constructor(opts: ClusterConnectOptions) { this.#startupNodes = opts.nodes.map((node) => new ClusterNode(node.hostname, node.port ?? 6379) ); this.#maxConnections = opts.maxConnections ?? 50; // TODO(uki00a): To be honest, I'm not sure if this default value is appropriate... this.#newRedis = opts.newRedis ?? connect; } get connection(): Connection { throw new NotImplementedError("Not implemented yet"); } exec(command: string, ...args: RedisValue[]): Promise { return this.sendCommand(command, args); } async sendCommand( command: string, _args?: RedisValue[], options?: SendCommandOptions, ): Promise { if (this.#refreshTableASAP) { await this.initializeSlotsCache(); } let asking = false; let askingNode: ClusterNode | null = null; let tryRandomNode = false; let ttl = kRedisClusterRequestTTL; let lastError: null | Error; const args = _args ?? []; while (ttl > 0) { ttl -= 1; const key = getKeyFromCommand(command, args); if (key == null) { throw new ClusterError( "No way to dispatch this command to Redis Cluster.", ); } const slot = calculateSlot(key); let r: Redis; if (asking && askingNode) { r = await this.#getConnectionByNode(askingNode); askingNode = null; } else if (tryRandomNode) { r = await this.#getRandomConnection(); tryRandomNode = false; } else { r = await this.#getConnectionBySlot(slot); } try { if (asking) { await r.asking(); } asking = false; const reply = await r.sendCommand(command, args, options); return reply; } catch (err) { if (err instanceof Error) { lastError = err; } else { throw err; // An unexpected error occurred. } if (err instanceof Deno.errors.BadResource) { tryRandomNode = true; if (ttl < kRedisClusterRequestTTL / 2) { await delay(100); } continue; } else if (err instanceof ErrorReplyError) { const [code, newSlot, ipAndPort] = err.message.split(/\s+/); if (code === "-MOVED" || code === "-ASK") { if (code === "-ASK") { asking = true; } else { // Server replied with MOVED. It's better for us to // ask for CLUSTER NODES the next time. this.#refreshTableASAP = true; } const node = ClusterNode.parseIPAndPort(ipAndPort); if (asking) { // Server replied with -ASK. We should send the next query to the redirected node. askingNode = node; } else { this.#nodeBySlot[parseInt(newSlot)] = node; } } else { throw err; } } else { throw err; // An unexpected error occurred. } } } throw new ClusterError( `Too many Cluster redirections? (last error: ${ lastError!.message ?? "" })`, ); } subscribe( _command: SubscribeCommand, ..._channelsOrPatterns: Array ): Promise> { return Promise.reject(new NotImplementedError("ClusterClient#subscribe")); } close(): void { const nodeNames = Object.keys(this.#connectionByNodeName); for (const nodeName of nodeNames) { const conn = this.#connectionByNodeName[nodeName]; if (conn) { conn.close(); delete this.#connectionByNodeName[nodeName]; } } this.#refreshTableASAP = true; } async initializeSlotsCache(): Promise { for (const node of this.#startupNodes) { try { const redis = await this.#getRedisLink(node); try { const clusterSlots = await redis.clusterSlots() as Array< [number, number, [string, number]] >; const nodes = [] as ClusterNode[]; const slotMap = {} as SlotMap; for (const [from, to, master] of clusterSlots) { for (let slot = from; slot <= to; slot++) { const [ip, port] = master; const node = new ClusterNode(ip, port); nodes.push(node); slotMap[slot] = node; } } this.#nodeBySlot = slotMap; await this.#populateStartupNodes(nodes); this.#refreshTableASAP = false; return; } finally { await redis.quit(); } } catch (_err) { // TODO: Consider logging `_err` here continue; } } } #populateStartupNodes(nodes: ClusterNode[]) { for (const node of nodes) { this.#startupNodes.push(node); } this.#startupNodes = distinctBy( this.#startupNodes, (node: ClusterNode) => node.name, ); } async #getRandomConnection(): Promise { for (const node of shuffle(this.#startupNodes)) { try { let conn = this.#connectionByNodeName[node.name]; if (conn) { const message = await conn.ping(); if (message === "PONG") { return conn; } } else { conn = await this.#getRedisLink(node); try { const message = await conn.ping(); if (message === "PONG") { await this.#closeExistingConnection(); this.#connectionByNodeName[node.name] = conn; return conn; } else { await conn.quit(); } } catch { conn.close(); } } } catch { // Just try with the next node. } } throw new ClusterError("Can't reach a single startup node."); } #getConnectionBySlot(slot: number): Promise { const node = this.#nodeBySlot[slot]; if (node) { return this.#getConnectionByNode(node); } else { return this.#getRandomConnection(); } } async #getConnectionByNode(node: ClusterNode): Promise { let conn = this.#connectionByNodeName[node.name]; if (conn) { return conn; } else { try { await this.#closeExistingConnection(); conn = await this.#getRedisLink(node); this.#connectionByNodeName[node.name] = conn; return conn; } catch { return this.#getRandomConnection(); } } } async #closeExistingConnection() { const nodeNames = Object.keys(this.#connectionByNodeName); while (nodeNames.length >= this.#maxConnections) { const nodeName = sample(nodeNames)!; const conn = this.#connectionByNodeName[nodeName]; delete this.#connectionByNodeName[nodeName]; try { await conn.quit(); } catch (err) { // deno-lint-ignore no-console -- TODO: consider improving logging console.error(err); } } } #getRedisLink(node: ClusterNode): Promise { const { hostname, port } = node; return this.#newRedis({ hostname, port }); } } function getKeyFromCommand(command: string, args: RedisValue[]): string | null { switch (command.toLowerCase()) { case "info": case "multi": case "exec": case "slaveof": case "config": case "shutdown": return null; default: return args[0] as string; } } /** * Connects to the Redis Cluster. * * @see https://redis.io/topics/cluster-tutorial * @see https://redis.io/topics/cluster-spec */ async function connectToCluster(opts: ClusterConnectOptions): Promise { const client = new ClusterClient(opts); await client.initializeSlotsCache(); return create(client); } export { connectToCluster as connect }; ================================================ FILE: experimental/pool/mod.ts ================================================ export * from "../../pool/mod.ts"; ================================================ FILE: experimental/web_streams_connection/mod.ts ================================================ /** * @module * @experimental **NOTE**: This is an unstable module. */ import { kUnstableCreateProtocol } from "../../internal/symbols.ts"; import type { Redis, RedisConnectOptions } from "../../redis.ts"; import { connect as _connect } from "../../redis.ts"; import { Protocol } from "../../protocol/web_streams/mod.ts"; function createProtocol(conn: Deno.Conn) { return new Protocol(conn); } export function connect(options: RedisConnectOptions): Promise { return _connect({ ...options, [kUnstableCreateProtocol]: createProtocol, }); } ================================================ FILE: import_map.dev.json ================================================ { "imports": { "benny": "npm:benny@3.7.1", "@uki00a/deno-json-lint": "jsr:@uki00a/deno-json-lint@^0.4.0" } } ================================================ FILE: import_map.test.json ================================================ { "imports": { "https://deno.land/x/redis/mod.ts": "./mod.ts", "https://deno.land/x/redis/experimental/cluster/mod.ts": "./experimental/cluster/mod.ts" } } ================================================ FILE: internal/buffered_readable_stream.ts ================================================ import { concateBytes } from "./concate_bytes.ts"; const LF = "\n".charCodeAt(0); /** * Wraps `ReadableStream` to provide buffering. Heavily inspired by `deno_std/io/buf_reader.ts`. * * {@link https://github.com/denoland/deno_std/blob/0.204.0/io/buf_reader.ts} */ export class BufferedReadableStream { #reader: ReadableStreamBYOBReader; #buffer: Uint8Array; constructor(readable: ReadableStream) { this.#reader = readable.getReader({ mode: "byob" }); this.#buffer = new Uint8Array(0); } async readLine(): Promise { const i = this.#buffer.indexOf(LF); if (i > -1) { return this.#consume(i + 1); } for (;;) { await this.#fill(); const i = this.#buffer.indexOf(LF); if (i > -1) return this.#consume(i + 1); } } async readN(n: number): Promise { if (n <= this.#buffer.length) { return this.#consume(n); } if (n === 0) { return new Uint8Array(0); } if (this.#buffer.length === 0) { const buffer = new Uint8Array(n); const { done, value } = await this.#reader.read(buffer, { min: buffer.length, }); if (done) { throw new Deno.errors.BadResource(); } return value; } else { const remaining = n - this.#buffer.length; const buffer = new Uint8Array(remaining); const { value, done } = await this.#reader.read(buffer, { min: remaining, }); if (done) { throw new Deno.errors.BadResource(); } const result = concateBytes(this.#buffer, value); this.#buffer = new Uint8Array(); return result; } } #consume(n: number): Uint8Array { const b = this.#buffer.subarray(0, n); this.#buffer = this.#buffer.subarray(n); return b; } async #fill() { const chunk = await this.#reader.read(new Uint8Array(1024)); if (chunk.done) { throw new Deno.errors.BadResource(); } const bytes = chunk.value; this.#buffer = concateBytes(this.#buffer, bytes); } } ================================================ FILE: internal/buffered_readable_stream_test.ts ================================================ import { encoder } from "./encoding.ts"; import { assertEquals, assertRejects } from "../deps/std/assert.ts"; import { BufferedReadableStream } from "./buffered_readable_stream.ts"; Deno.test({ name: "BufferedReadableStream", permissions: "none", fn: async (t) => { const decoder = new TextDecoder(); await t.step("readLine", async () => { const readable = createReadableStreamFromString( "*2\r\n$5\r\nhello\r\n:1234\r\n", ); const buffered = new BufferedReadableStream(readable); assertEquals(decoder.decode(await buffered.readLine()), "*2\r\n"); assertEquals(decoder.decode(await buffered.readLine()), "$5\r\n"); assertEquals(decoder.decode(await buffered.readLine()), "hello\r\n"); assertEquals(decoder.decode(await buffered.readLine()), ":1234\r\n"); await assertRejects(() => buffered.readLine(), Deno.errors.BadResource); }); await t.step("readN", async () => { const readable = createReadableStreamFromString( "$12\r\nhello_world!\r\n", ); const buffered = new BufferedReadableStream(readable); await buffered.readN(0); assertEquals(decoder.decode(await buffered.readLine()), "$12\r\n"); { const buf = await buffered.readN(5); assertEquals(decoder.decode(buf), "hello"); } await buffered.readN(0); { const buf = await buffered.readN(7); assertEquals(decoder.decode(buf), "_world!"); } await buffered.readN(0); { const buf = await buffered.readN(2); assertEquals(decoder.decode(buf), "\r\n"); } await buffered.readN(0); await assertRejects( () => buffered.readN(1), Deno.errors.BadResource, ); }); await t.step( "`readN` should not throw `RangeError: offset is out of bounds` error", async () => { const readable = new ReadableStream({ type: "bytes", start(controller) { controller.enqueue(encoder.encode("foobar")); controller.close(); }, }); const buffered = new BufferedReadableStream(readable); { const buf = await buffered.readN(3); assertEquals(decoder.decode(buf), "foo"); } { const buf = await buffered.readN(1); assertEquals(decoder.decode(buf), "b"); } await buffered.readN(0); { const buf = await buffered.readN(2); assertEquals(decoder.decode(buf), "ar"); } }, ); }, }); function createReadableStreamFromString(s: string): ReadableStream { const encoder = new TextEncoder(); let numRead = 0; return new ReadableStream({ type: "bytes", pull(controller) { controller.enqueue(encoder.encode(s[numRead])); numRead++; if (numRead >= s.length) { controller.close(); } }, }); } ================================================ FILE: internal/concate_bytes.ts ================================================ export function concateBytes(a: Uint8Array, b: Uint8Array) { if (a.length === 0) return b; const size = a.length + b.length; const buf = new Uint8Array(size); buf.set(a); buf.set(b, a.length); return buf; } ================================================ FILE: internal/concate_bytes_test.ts ================================================ import { assertEquals } from "../deps/std/assert.ts"; import { concateBytes } from "./concate_bytes.ts"; Deno.test("concateBytes", () => { const encoder = new TextEncoder(); const decoder = new TextDecoder(); const e = (s: string) => encoder.encode(s); const d = (b: Uint8Array) => decoder.decode(b); assertEquals(d(concateBytes(e("foo"), e("bar"))), "foobar"); assertEquals(d(concateBytes(e(""), e(""))), ""); assertEquals(d(concateBytes(e(""), e("hello"))), "hello"); }); ================================================ FILE: internal/delegate.ts ================================================ type Delegate = Pick extends Record< string | symbol, // deno-lint-ignore ban-types -- This is used only as a constraint on the type argument Function > ? Pick : never; export function delegate< TObject extends object, TMethods extends keyof TObject, >( target: TObject, methods: Array, ): Delegate { return methods.reduce((proxy, method) => { if (typeof target[method] === "function") { proxy[method] = target[method].bind(target); } else { throw new Error(`${String(method)} should be a method`); } return proxy; }, {} as Delegate); } ================================================ FILE: internal/delegate_test.ts ================================================ import { delegate } from "./delegate.ts"; import { assert, assertExists, assertFalse, assertNotStrictEquals, } from "../deps/std/assert.ts"; import type { Has, IsExact, NotHas } from "../deps/std/testing.ts"; import { assertType } from "../deps/std/testing.ts"; Deno.test("delegate", () => { class Connection { #isConnected = false; connect(): void { this.#isConnected = true; } close(): void { this.#isConnected = false; } isConnected(): boolean { return this.#isConnected; } isClosed(): boolean { return !this.#isConnected; } } const base = new Connection(); const proxy = delegate(base, ["connect", "isConnected"]); assertNotStrictEquals(proxy.connect, base.connect); assertNotStrictEquals(proxy.isConnected, base.isConnected); const kClose = "close"; assert( // @ts-expect-error - `close()` should not be defined. proxy[kClose] === undefined, ); assertExists(base[kClose]); assertType>(true); assertType>(true); assertType>(true); assertType>( true, ); assertFalse(base.isConnected()); assertFalse(proxy.isConnected()); proxy.connect(); assert(base.isConnected()); assert(proxy.isConnected()); }); ================================================ FILE: internal/encoding.ts ================================================ export const encoder = new TextEncoder(); export const decoder = new TextDecoder(); ================================================ FILE: internal/on.ts ================================================ interface Options { signal?: AbortSignal; } /** * Converts {@linkcode EventTarget} to {@linkcode AsyncIterableIterator}, similar to `on()` in `node:events`. */ export function on( eventTarget: EventTarget, event: string, options: Options = {}, ): AsyncIterableIterator { // TODO: Optimize the implementation. const abortController = new AbortController(); const signal = options.signal ? AbortSignal.any([options.signal, abortController.signal]) : abortController.signal; const readerQueue: Array> = []; const bufferedEventQueue: Array = []; if (!signal.aborted) { eventTarget.addEventListener( event, (event) => { if (readerQueue.length) { const { resolve } = readerQueue.shift()!; resolve(event); } else { bufferedEventQueue.push(event); } }, { signal }, ); } function cleanup(): void { for (const d of readerQueue) { d.reject(signal.reason); } readerQueue.length = 0; } const iter: AsyncIterableIterator = { [Symbol.asyncIterator]() { return this; }, async next() { if (signal.aborted) { return { done: true, value: undefined }; } else if (bufferedEventQueue.length) { const event = bufferedEventQueue.shift()!; return { done: false, value: event }; } else { const deferred = Promise.withResolvers(); readerQueue.push(deferred); const value = await deferred.promise; return { done: false, value }; } }, return() { abortController.abort(); cleanup(); return Promise.resolve({ done: true, value: undefined }); }, }; return iter; } ================================================ FILE: internal/on_test.ts ================================================ import { on } from "./on.ts"; import { assert, assertEquals, assertStrictEquals, } from "../deps/std/assert.ts"; Deno.test({ name: "on", permissions: "none", fn: async (t) => { await t.step("implements [Symbol.asyncIterator]", async () => { const eventType = "foo"; const target = new EventTarget(); const ac = new AbortController(); const iter = on(target, eventType, { signal: ac.signal }); const events: Array = []; const promise = (async () => { for await (const event of iter) { assertStrictEquals(event.type, eventType); events.push(event); if (events.length > 2) { ac.abort(); } } })(); target.dispatchEvent(new CustomEvent(eventType)); target.dispatchEvent(new CustomEvent(eventType + "bar")); target.dispatchEvent(new CustomEvent(eventType)); target.dispatchEvent(new CustomEvent(eventType)); await promise; assertEquals(await iter.next(), { done: true, value: undefined }); assertStrictEquals(events.length, 3); }); await t.step("implements Symbol.asyncIterator#return()", async () => { const eventType = "bar"; const target = new EventTarget(); const ac = new AbortController(); const iter = on(target, eventType, { signal: ac.signal }); target.dispatchEvent(new CustomEvent(eventType)); const result = await iter.next(); assertStrictEquals(result.done, false); assertStrictEquals(result.value.type, eventType); assert(iter.return != null); iter.return(); assertEquals(await iter.next(), { done: true, value: undefined }); target.dispatchEvent(new CustomEvent(eventType)); assertEquals(await iter.next(), { done: true, value: undefined }); }); }, }); ================================================ FILE: internal/symbols.ts ================================================ /** * @private */ export const kUnstableReadReply = Symbol("deno-redis.readReply"); /** * @private */ export const kUnstableWriteCommand = Symbol("deno-redis.writeCommand"); /** * @private */ export const kUnstablePipeline = Symbol("deno-redis.pipeline"); /** * @private */ export const kUnstableCreateProtocol = Symbol("deno-redis.createProtocol"); /** * @private */ export const kUnstableProtover = Symbol("deno-redis.protover"); /** * @private */ export const kUnstableStartReadLoop = Symbol("deno-redis.startReadLoop"); ================================================ FILE: internal/typed_event_target.ts ================================================ export interface TypedEventTarget> extends Omit< EventTarget, "addEventListener" | "removeEventListener" | "dispatchEvent" > { addEventListener( type: K, callback: ( event: CustomEvent, ) => void, options?: AddEventListenerOptions | boolean, ): void; removeEventListener( type: K, callback: ( event: CustomEvent, ) => void, options?: EventListenerOptions | boolean, ): void; } export function createTypedEventTarget< TEventMap extends Record, >(): TypedEventTarget { return new EventTarget() as TypedEventTarget; } export function dispatchEvent< TEventMap extends Record, TKey extends Extract, >( eventTarget: TypedEventTarget, event: TKey, detail: TEventMap[TKey], ): boolean { return (eventTarget as EventTarget).dispatchEvent( new CustomEvent(event, { detail, }), ); } ================================================ FILE: mod.ts ================================================ // Generated by tools/make_mod.ts. Don't edit. export { okReply } from "./protocol/shared/types.ts"; export { connect, create, createLazyClient, parseURL } from "./redis.ts"; export { ConnectionClosedError, EOFError, ErrorReplyError, InvalidStateError, NotImplementedError, SubscriptionClosedError, } from "./errors.ts"; export type { Backoff, ExponentialBackoffOptions } from "./backoff.ts"; export type { ACLLogMode, BitfieldOpts, BitfieldWithOverflowOpts, ClientCachingMode, ClientKillOpts, ClientListOpts, ClientPauseMode, ClientTrackingOpts, ClientType, ClientUnblockingBehaviour, ClusterFailoverMode, ClusterResetMode, ClusterSetSlotSubcommand, GeoRadiusOpts, GeoUnit, HelloOpts, HScanOpts, LInsertLocation, LPosOpts, LPosWithCountOpts, MemoryUsageOpts, MigrateOpts, RedisCommands, RestoreOpts, ScanOpts, ScriptDebugMode, SetOpts, SetReply, SetWithModeOpts, ShutdownMode, SortOpts, SortWithDestinationOpts, SScanOpts, StralgoAlgorithm, StralgoOpts, StralgoTarget, ZAddOpts, ZAddReply, ZInterOpts, ZInterstoreOpts, ZRangeByLexOpts, ZRangeByScoreOpts, ZRangeOpts, ZScanOpts, ZUnionstoreOpts, } from "./command.ts"; export type { Connection, RedisConnectionOptions, SendCommandOptions, } from "./connection.ts"; export type { ConnectionErrorEventDetails, ConnectionEvent, ConnectionEventType, ConnectionReconnectingEventDetails, } from "./events.ts"; export type { Client } from "./client.ts"; export type { RedisPubSubMessage, RedisSubscription } from "./subscription.ts"; export type { CommandExecutor } from "./executor.ts"; export type { RedisPipeline } from "./pipeline.ts"; export type { Binary, Bulk, BulkNil, BulkString, ConditionalArray, Integer, Raw, RawOrError, RedisReply, RedisValue, SimpleString, } from "./protocol/shared/types.ts"; export type { Redis, RedisConnectOptions } from "./redis.ts"; export type { StartEndCount, XAddFieldValues, XClaimJustXId, XClaimMessages, XClaimOpts, XClaimReply, XConsumerDetail, XGroupDetail, XId, XIdAdd, XIdCreateGroup, XIdGroupRead, XIdInput, XIdNeg, XIdPos, XInfoConsumer, XInfoConsumersReply, XInfoGroup, XInfoGroupsReply, XInfoStreamFullReply, XInfoStreamReply, XKeyId, XKeyIdGroup, XKeyIdGroupLike, XKeyIdLike, XMaxlen, XMessage, XPendingConsumer, XPendingCount, XPendingReply, XReadGroupOpts, XReadIdData, XReadOpts, XReadReply, XReadReplyRaw, XReadStream, XReadStreamRaw, } from "./stream.ts"; ================================================ FILE: pipeline.ts ================================================ import type { Connection, SendCommandOptions } from "./connection.ts"; import { kEmptyRedisArgs } from "./protocol/shared/command.ts"; import type { Client } from "./client.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisSubscription, SubscribeCommand, } from "./subscription.ts"; import type { RawOrError, RedisReply, RedisValue, } from "./protocol/shared/types.ts"; import { okReply } from "./protocol/shared/types.ts"; import type { Redis } from "./redis.ts"; import { create } from "./redis.ts"; import { kUnstablePipeline } from "./internal/symbols.ts"; export interface RedisPipeline extends Redis { flush(): Promise; } export function createRedisPipeline( connection: Connection, tx = false, ): RedisPipeline { const pipeline = new PipelineClient(connection, tx); function flush(): Promise { return pipeline.flush(); } const client = create(pipeline); return Object.assign(client, { flush }); } class PipelineClient implements Client { private commands: { command: string; args: RedisValue[]; returnUint8Arrays?: boolean; }[] = []; private queue: { commands: { command: string; args: RedisValue[]; returnUint8Arrays?: boolean; }[]; resolve: (value: RawOrError[]) => void; reject: (error: unknown) => void; }[] = []; constructor( readonly connection: Connection, private tx: boolean, ) { } exec( command: string, ...args: RedisValue[] ): Promise { return this.sendCommand(command, args); } sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ): Promise { this.commands.push({ command, args: args ?? kEmptyRedisArgs, returnUint8Arrays: options?.returnUint8Arrays, }); return Promise.resolve(okReply); } close(): void { return this.connection.close(); } subscribe( _command: SubscribeCommand, ..._channelsOrPatterns: Array ): Promise> { return Promise.reject(new Error("Pub/Sub cannot be used with a pipeline")); } flush(): Promise { if (this.tx) { this.commands.unshift({ command: "MULTI", args: [] }); this.commands.push({ command: "EXEC", args: [] }); } const { promise, resolve, reject } = Promise.withResolvers(); this.queue.push({ commands: [...this.commands], resolve, reject }); if (this.queue.length === 1) { this.dequeue(); } this.commands = []; return promise; } private dequeue(): void { const [e] = this.queue; if (!e) return; this.connection[kUnstablePipeline](e.commands) .then(e.resolve) .catch(e.reject) .finally(() => { this.queue.shift(); this.dequeue(); }); } } ================================================ FILE: pool/client.ts ================================================ import type { Connection, SendCommandOptions } from "../connection.ts"; import type { Pool } from "./pool.ts"; import type { Client } from "../client.ts"; import type { DefaultPubSubMessageType, PubSubMessageType, RedisSubscription, SubscribeCommand, } from "../subscription.ts"; import { createDefaultClient } from "../default_client.ts"; import { kUnstablePipeline, kUnstableReadReply, kUnstableStartReadLoop, kUnstableWriteCommand, } from "../internal/symbols.ts"; import { delegate } from "../internal/delegate.ts"; import type { RedisReply, RedisValue } from "../protocol/shared/types.ts"; export function createPoolClient(pool: Pool): Client { return new PoolClient(pool); } class PoolClient implements Client { readonly #pool: Pool; constructor(pool: Pool) { this.#pool = pool; } get connection(): Connection { throw new Error("PoolClient.connection is not supported"); } async exec( command: string, ...args: RedisValue[] ): Promise { const connection = await this.#pool.acquire(); try { const client = createDefaultClient(connection); return await client.exec(command, ...args); } finally { this.#pool.release(connection); } } async sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ): Promise { const connection = await this.#pool.acquire(); try { const client = createDefaultClient(connection); return await client.sendCommand(command, args, options); } finally { this.#pool.release(connection); } } async subscribe< TMessage extends PubSubMessageType = DefaultPubSubMessageType, >( command: SubscribeCommand, ...channelsOrPatterns: Array ): Promise> { const connection = await this.#pool.acquire(); const client = createDefaultClient( createPoolConnection(this.#pool, connection), ); try { const subscription = await client.subscribe( command, ...channelsOrPatterns, ); return subscription; } catch (error) { this.#pool.release(connection); throw error; } } close(): void { return this.#pool.close(); } } function createPoolConnection( pool: Pool, connection: Connection, ): Connection { function close(): void { return pool.release(connection); } return { ...delegate(connection, [ "connect", "reconnect", "sendCommand", "addEventListener", "removeEventListener", Symbol.dispose, kUnstableReadReply, kUnstableWriteCommand, kUnstablePipeline, kUnstableStartReadLoop, ]), close, get name() { return connection.name; }, get isConnected() { return connection.isConnected; }, get isClosed() { return connection.isClosed; }, }; } ================================================ FILE: pool/default_pool.ts ================================================ import type { Pool } from "./pool.ts"; class AlreadyRemovedFromPoolError extends Error { constructor() { super("This connection has already been removed from the pool."); } } const kDefaultTimeout = 5_000; class DefaultPool implements Pool { readonly #idle: Array = []; readonly #connections: Array = []; #connectionCount: number = 0; readonly #deferredQueue: Array> = []; readonly #options: Required>; constructor( { maxConnections = 8, acquire, }: PoolOptions, ) { this.#options = { acquire, maxConnections, }; } async acquire(signal?: AbortSignal): Promise { signal ||= AbortSignal.timeout(kDefaultTimeout); signal.throwIfAborted(); if (this.#idle.length > 0) { const conn = this.#idle.shift()!; return Promise.resolve(conn); } if (this.#connectionCount < this.#options.maxConnections) { this.#connectionCount++; try { const connection = await this.#options.acquire(); this.#connections.push(connection); return connection; } catch (error) { this.#connectionCount--; throw error; } } const deferred = Promise.withResolvers(); this.#deferredQueue.push(deferred); const { promise, reject } = deferred; const onAbort = () => { const i = this.#deferredQueue.indexOf(deferred); if (i === -1) return; this.#deferredQueue.splice(i, 1); reject(signal.reason); }; signal.addEventListener("abort", onAbort, { once: true }); return promise; } #has(conn: T): boolean { return this.#connections.includes(conn); } release(conn: T): void { if (!this.#has(conn)) { throw new AlreadyRemovedFromPoolError(); } else if (this.#deferredQueue.length > 0) { const i = this.#deferredQueue.shift()!; i.resolve(conn); } else { this.#idle.push(conn); } } close() { const errors: Array = []; for (const x of [...this.#connections]) { try { x[Symbol.dispose](); } catch (error) { errors.push(error); } } this.#connections.length = 0; this.#idle.length = 0; if (errors.length > 0) { throw new AggregateError(errors); } } } export interface PoolOptions { maxConnections?: number; acquire(): Promise; } export function createDefaultPool( options: PoolOptions, ): Pool { return new DefaultPool(options); } ================================================ FILE: pool/default_pool_test.ts ================================================ import { assert, assertEquals, assertRejects } from "../deps/std/assert.ts"; import { createDefaultPool } from "./default_pool.ts"; class FakeConnection implements Disposable { #isClosed = false; isClosed() { return this.#isClosed; } [Symbol.dispose]() { if (this.#isClosed) { throw new Error("Already closed"); } this.#isClosed = true; } } Deno.test({ name: "DefaultPool", permissions: "none", fn: async () => { const openConnections: Array = []; const pool = createDefaultPool({ acquire: () => { const connection = new FakeConnection(); openConnections.push(connection); return Promise.resolve(connection); }, maxConnections: 2, }); assertEquals(openConnections, []); const signal = AbortSignal.timeout(200); const conn1 = await pool.acquire(signal); assertEquals(openConnections, [conn1]); assert(openConnections.every((x) => !x.isClosed())); assert(!signal.aborted); const conn2 = await pool.acquire(signal); assertEquals(openConnections, [conn1, conn2]); assert(!conn2.isClosed()); assert(openConnections.every((x) => !x.isClosed())); assert(!signal.aborted); { // Tests timeout handling await assertRejects( () => pool.acquire(signal), "Intentionally aborted", ); assert(signal.aborted); assertEquals(openConnections, [conn1, conn2]); assert(openConnections.every((x) => !x.isClosed())); } { // Tests `release()` pool.release(conn2); assertEquals(openConnections, [conn1, conn2]); const conn = await pool.acquire(new AbortController().signal); assert(conn === conn2, "A new connection should not be created"); assertEquals(openConnections, [conn1, conn2]); } { // `Pool#acquire` should wait for an active connection to be released. const signal = AbortSignal.timeout(3_000); const promise = pool.acquire(signal); setTimeout(() => { pool.release(conn1); }, 50); const conn = await promise; assert(conn === conn1, "A new connection should not be created"); assertEquals(openConnections, [conn1, conn2]); assert(!signal.aborted); } { // `Pool#close` closes all connections pool.close(); assert(openConnections.every((x) => x.isClosed())); } }, }); ================================================ FILE: pool/mod.ts ================================================ import type { Redis, RedisConnectOptions } from "../redis.ts"; import { create } from "../redis.ts"; import type { Connection } from "../connection.ts"; import { createRedisConnection } from "../default_connection.ts"; import { createDefaultPool } from "./default_pool.ts"; import { createPoolClient as baseCreatePoolClient } from "./client.ts"; export interface CreatePoolClientOptions { connection: RedisConnectOptions; maxConnections?: number; } export function createPoolClient( options: CreatePoolClientOptions, ): Promise { const pool = createDefaultPool({ acquire, maxConnections: options.maxConnections ?? 8, }); const client = create(baseCreatePoolClient(pool)); return Promise.resolve(client); async function acquire(): Promise { const { hostname, port, ...connectionOptions } = options.connection; const connection = createRedisConnection(hostname, port, connectionOptions); await connection.connect(); return connection; } } ================================================ FILE: pool/pool.ts ================================================ export interface Pool { acquire(signal?: AbortSignal): Promise; release(item: T): void; close(): void; } ================================================ FILE: protocol/deno_streams/command.ts ================================================ import type { BufReader, BufWriter } from "../../deps/std/io.ts"; import { readReply } from "./reply.ts"; import { ErrorReplyError } from "../../errors.ts"; import type { RedisReply, RedisValue } from "../shared/types.ts"; import { encodeCommand } from "../shared/command.ts"; import type { Command } from "../shared/protocol.ts"; export async function writeCommand( writer: BufWriter, command: string, args: RedisValue[], ) { const request = encodeCommand(command, args); await writer.write(request); } export async function sendCommand( writer: BufWriter, reader: BufReader, command: string, args: RedisValue[], returnUint8Arrays?: boolean, ): Promise { await writeCommand(writer, command, args); await writer.flush(); return readReply(reader, returnUint8Arrays); } export async function sendCommands( writer: BufWriter, reader: BufReader, commands: Command[], ): Promise<(RedisReply | ErrorReplyError)[]> { for (const { command, args } of commands) { await writeCommand(writer, command, args); } await writer.flush(); const ret: (RedisReply | ErrorReplyError)[] = []; for (let i = 0; i < commands.length; i++) { try { const rep = await readReply(reader, commands[i].returnUint8Arrays); ret.push(rep); } catch (e) { if (e instanceof ErrorReplyError) { ret.push(e); } else { throw e; } } } return ret; } ================================================ FILE: protocol/deno_streams/mod.ts ================================================ import { BufReader, BufWriter } from "../../deps/std/io.ts"; import { readReply } from "./reply.ts"; import { sendCommand, sendCommands, writeCommand } from "./command.ts"; import type { Command, Protocol as BaseProtocol } from "../shared/protocol.ts"; import type { RedisReply, RedisValue } from "../shared/types.ts"; import type { ErrorReplyError } from "../../errors.ts"; export class Protocol implements BaseProtocol { #reader: BufReader; #writer: BufWriter; constructor(conn: Deno.Conn) { this.#reader = new BufReader(conn); this.#writer = new BufWriter(conn); } sendCommand( command: string, args: RedisValue[], returnsUint8Arrays?: boolean | undefined, ): Promise { return sendCommand( this.#writer, this.#reader, command, args, returnsUint8Arrays, ); } readReply(returnsUint8Arrays?: boolean): Promise { return readReply(this.#reader, returnsUint8Arrays); } async writeCommand(command: Command): Promise { await writeCommand(this.#writer, command.command, command.args); await this.#writer.flush(); } pipeline(commands: Command[]): Promise> { return sendCommands(this.#writer, this.#reader, commands); } } ================================================ FILE: protocol/deno_streams/reply.ts ================================================ import type { BufReader } from "../../deps/std/io.ts"; import type * as types from "../shared/types.ts"; import { ArrayReplyCode, AttributeReplyCode, BigNumberReplyCode, BlobErrorReplyCode, BooleanReplyCode, BulkReplyCode, DoubleReplyCode, ErrorReplyCode, IntegerReplyCode, MapReplyCode, NullReplyCode, PushReplyCode, SetReplyCode, SimpleStringCode, VerbatimStringCode, } from "../shared/reply.ts"; import { EOFError, ErrorReplyError, InvalidStateError } from "../../errors.ts"; import { decoder } from "../../internal/encoding.ts"; export async function readReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { const res = await reader.peek(1); if (res == null) { throw new EOFError(); } const code = res[0]; if (code === ErrorReplyCode) { await readErrorReplyOrFail(reader); } switch (code) { case IntegerReplyCode: return readIntegerReply(reader); case SimpleStringCode: return readSimpleStringReply(reader, returnUint8Arrays); case BulkReplyCode: return readBulkReply(reader, returnUint8Arrays); case ArrayReplyCode: return readArrayReply(reader, returnUint8Arrays); case MapReplyCode: return readMapReply(reader, returnUint8Arrays); case SetReplyCode: return readSetReply(reader, returnUint8Arrays); case BooleanReplyCode: return readBooleanReply(reader); case DoubleReplyCode: return readDoubleReply(reader, returnUint8Arrays); case BigNumberReplyCode: return readBigNumberReply(reader, returnUint8Arrays); case VerbatimStringCode: return readVerbatimStringReply(reader, returnUint8Arrays); case NullReplyCode: return readNullReply(reader); case PushReplyCode: return readPushReply(reader, returnUint8Arrays); case AttributeReplyCode: { await readAttributeReply(reader); return readReply(reader, returnUint8Arrays); } case BlobErrorReplyCode: { const body = (await readBlobReply(reader, BlobErrorReplyCode)) as string; throw new ErrorReplyError(body); } default: throw new InvalidStateError( `unknown code: '${String.fromCharCode(code)}' (${code})`, ); } } async function readIntegerReply( reader: BufReader, ): Promise { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } return Number.parseInt(decoder.decode(line.subarray(1, line.length))); } function readBulkReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { return readBlobReply(reader, BulkReplyCode, returnUint8Arrays); } function readVerbatimStringReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { return readBlobReply(reader, VerbatimStringCode, returnUint8Arrays); } function readSimpleStringReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { return readSingleLineReply(reader, SimpleStringCode, returnUint8Arrays); } function readArrayReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise | null> { return readArrayLikeReply(reader, returnUint8Arrays); } function readPushReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise | null> { return readArrayLikeReply(reader, returnUint8Arrays); } async function readArrayLikeReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise | null> { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } const argCount = parseSize(line); if (argCount === -1) { // `-1` indicates a null array return null; } const array: Array = []; for (let i = 0; i < argCount; i++) { array.push(await readReply(reader, returnUint8Arrays)); } return array; } /** * NOTE: We treat a set type as an array to keep backward compatibility. */ async function readSetReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise | null> { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } const size = parseSize(line); if (size === -1) { // `-1` indicates a null set return null; } const set: Array = []; for (let i = 0; i < size; i++) { set.push(await readReply(reader, returnUint8Arrays)); } return set; } /** * NOTE: We treat a map type as an array to keep backward compatibility. */ async function readMapReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise | null> { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } const numberOfFieldValuePairs = parseSize(line); if (numberOfFieldValuePairs === -1) { return null; } const entries: Array = []; for (let i = 0; i < (numberOfFieldValuePairs * 2); i++) { entries.push(await readReply(reader, returnUint8Arrays)); } return entries; } /** * NOTE: Currently, we simply drop attributes. * TODO: Provide a way for users to capture attributes. */ async function readAttributeReply( reader: BufReader, ): Promise { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } const numberOfAttributes = parseSize(line); if (numberOfAttributes === -1) { return; } for (let i = 0; i < numberOfAttributes; i++) { await readReply(reader); // Reads a key await readReply(reader); // Raads a value } } async function readBooleanReply(reader: BufReader): Promise<1 | 0> { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } const isTrue = line[1] === 116; return isTrue ? 1 // `#t` : 0; // `#f` } function readDoubleReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { return readSingleLineReply(reader, DoubleReplyCode, returnUint8Arrays); } function readBigNumberReply( reader: BufReader, returnUint8Arrays?: boolean, ): Promise { return readSingleLineReply(reader, BigNumberReplyCode, returnUint8Arrays); } async function readNullReply(reader: BufReader): Promise { // Discards the current line await readLine(reader); return null; } async function readSingleLineReply( reader: BufReader, expectedCode: number, returnUint8Arrays?: boolean, ): Promise { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } if (line[0] !== expectedCode) { parseErrorReplyOrFail(line); } const body = line.subarray(1); return returnUint8Arrays ? body : decoder.decode(body); } type BlobLikeReply = string | types.Binary | null; async function readBlobReply( reader: BufReader, expectedCode: number, returnUint8Arrays?: boolean, ): Promise { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } if (line[0] !== expectedCode) { parseErrorReplyOrFail(line); } const size = parseSize(line); if (size < 0) { // nil bulk reply return null; } const dest = new Uint8Array(size + 2); await reader.readFull(dest); const body = dest.subarray(0, dest.length - 2); // Strip CR and LF return returnUint8Arrays ? body : decoder.decode(body); } function parseErrorReplyOrFail(line: Uint8Array): never { const code = line[0]; if (code === ErrorReplyCode) { throw new ErrorReplyError(decoder.decode(line)); } throw new Error(`invalid line: ${line}`); } async function readErrorReplyOrFail(reader: BufReader): Promise { const line = await readLine(reader); if (line == null) { throw new InvalidStateError(); } parseErrorReplyOrFail(line); } async function readLine(reader: BufReader): Promise { let result = await reader.readLine(); if (result == null) { throw new InvalidStateError(); } let line = result.line; while (result?.more) { result = await reader.readLine(); if (result == null) { throw new InvalidStateError(); } const mergedLine = new Uint8Array(line.length + result.line.length); mergedLine.set(line); mergedLine.set(result.line, line.length); line = mergedLine; } return line; } function parseSize(line: Uint8Array): number { const sizeStr = line.subarray(1, line.length); const size = parseInt(decoder.decode(sizeStr)); return size; } ================================================ FILE: protocol/deno_streams/reply_test.ts ================================================ import { assertEquals, assertRejects } from "../../deps/std/assert.ts"; import { BufReader, readerFromStreamReader } from "../../deps/std/io.ts"; import { ErrorReplyError } from "../../errors.ts"; import { readReply } from "./reply.ts"; import type { RedisReply } from "../shared/types.ts"; Deno.test({ name: "readReply", permissions: "none", fn: async (t) => { await t.step("array", async () => { const encoder = new TextEncoder(); const rawReply = "*4\r\n$3\r\nfoo\r\n_\r\n(123456789\r\n:456\r\n"; const readable = ReadableStream.from([encoder.encode(rawReply)]); const reader = readable.getReader(); const reply = await readReply( BufReader.create(readerFromStreamReader(reader)), ); assertEquals(reply, ["foo", null, "123456789", 456]); }); await t.step("blob error", async () => { const encoder = new TextEncoder(); const rawReply = "!21\r\nSYNTAX invalid syntax\r\n_\r\n"; const readable = ReadableStream.from([encoder.encode(rawReply)]); const reader = readable.getReader(); const buf = BufReader.create(readerFromStreamReader(reader)); await assertRejects( () => readReply(buf), ErrorReplyError, "SYNTAX invalid syntax", ); assertEquals(await readReply(buf), null); }); await t.step("attribute", async () => { const encoder = new TextEncoder(); const cases: Array<[given: string, expected: RedisReply]> = [ [ "|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", "Hello World", ], [ "*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", [123, "str", "1.23"], ], ]; for ( const [given, expected] of cases ) { const readable = ReadableStream.from([encoder.encode(given)]); const reader = readable.getReader(); const buf = BufReader.create(readerFromStreamReader(reader)); const actual = await readReply(buf); assertEquals(actual, expected); } }); }, }); ================================================ FILE: protocol/shared/command.ts ================================================ import { concat } from "../../deps/std/bytes.ts"; import { encoder } from "../../internal/encoding.ts"; import type { RedisValue } from "./types.ts"; import type { Command } from "./protocol.ts"; const CRLF = encoder.encode("\r\n"); const ArrayCode = encoder.encode("*"); const BulkCode = encoder.encode("$"); const kEmptyBuffer = new Uint8Array(0); export const kEmptyRedisArgs: Array = []; export function encodeCommand( command: string, args: RedisValue[], ): Uint8Array { const encodedArgsCount = encoder.encode( String(1 + args.length), ); const encodedCommand = encoder.encode(command); const encodedCommandLength = encoder.encode( String(encodedCommand.byteLength), ); let totalBytes = ArrayCode.byteLength + encodedArgsCount.byteLength + CRLF.byteLength + BulkCode.byteLength + encodedCommandLength.byteLength + CRLF.byteLength + encodedCommand.byteLength + CRLF.byteLength; const encodedArgs: Array = Array(args.length); for (let i = 0; i < args.length; i++) { const arg = args[i]; const bytes = arg instanceof Uint8Array ? arg : (arg == null ? kEmptyBuffer : encoder.encode(String(arg))); const bytesLen = bytes.byteLength; totalBytes += BulkCode.byteLength + String(bytesLen).length + CRLF.byteLength + bytes.byteLength + CRLF.byteLength; encodedArgs[i] = bytes; } const request = new Uint8Array(totalBytes); let index = 0; index = writeFrom(request, ArrayCode, index); index = writeFrom(request, encodedArgsCount, index); index = writeFrom(request, CRLF, index); index = writeFrom(request, BulkCode, index); index = writeFrom(request, encodedCommandLength, index); index = writeFrom(request, CRLF, index); index = writeFrom(request, encodedCommand, index); index = writeFrom(request, CRLF, index); for (let i = 0; i < encodedArgs.length; i++) { const encodedArg = encodedArgs[i]; const encodedArgLength = encoder.encode(String(encodedArg.byteLength)); index = writeFrom(request, BulkCode, index); index = writeFrom(request, encodedArgLength, index); index = writeFrom(request, CRLF, index); index = writeFrom(request, encodedArg, index); index = writeFrom(request, CRLF, index); } return request; } function writeFrom( bytes: Uint8Array, payload: Uint8Array, fromIndex: number, ): number { bytes.set(payload, fromIndex); return fromIndex + payload.byteLength; } export function encodeCommands(commands: Array): Uint8Array { // TODO: find a more optimized solution. const bufs: Array = Array(commands.length); for (let i = 0; i < commands.length; i++) { const { command, args } = commands[i]; bufs[i] = encodeCommand(command, args); } return concat(bufs); } ================================================ FILE: protocol/shared/command_test.ts ================================================ import { encodeCommand } from "./command.ts"; import { assertEquals } from "../../deps/std/assert.ts"; Deno.test({ name: "encodeCommand", permissions: "none", fn: () => { const actual = encodeCommand("SET", ["name", "bar"]); const expected = new TextEncoder().encode( "*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$3\r\nbar\r\n", ); assertEquals(actual, expected); }, }); ================================================ FILE: protocol/shared/protocol.ts ================================================ import type { RedisReply, RedisValue } from "./types.ts"; import type { ErrorReplyError } from "../../errors.ts"; export interface Command { command: string; args: RedisValue[]; returnUint8Arrays?: boolean; } export interface Protocol { sendCommand( command: string, args: Array, returnsUint8Arrays?: boolean, ): Promise; readReply(returnsUint8Array?: boolean): Promise; writeCommand(command: Command): Promise; pipeline( commands: Array, ): Promise>; } ================================================ FILE: protocol/shared/reply.ts ================================================ /** * Represents a number in RESP2/RESP3. */ export const IntegerReplyCode = ":".charCodeAt(0); /** * Represents a double which is introduced in RESP3. */ export const DoubleReplyCode = ",".charCodeAt(0); /** * Represents a blob string in RESP2/RESP3. */ export const BulkReplyCode = "$".charCodeAt(0); export const SimpleStringCode = "+".charCodeAt(0); /** Represents a verbatim string in RESP3. */ export const VerbatimStringCode = "=".charCodeAt(0); export const ArrayReplyCode = "*".charCodeAt(0); /** * Represents a simple error in RESP2/RESP3. */ export const ErrorReplyCode = "-".charCodeAt(0); /** Represents a blob error which is introduced in RESP3. */ export const BlobErrorReplyCode = "!".charCodeAt(0); /** Represents a map which is introduced in RESP3. */ export const MapReplyCode = "%".charCodeAt(0); /** Represents a set which is introduced in RESP3. */ export const SetReplyCode = "~".charCodeAt(0); /** Represents a boolean which is introduced in RESP3. */ export const BooleanReplyCode = "#".charCodeAt(0); /** Represents a big number which is introduced in RESP3. */ export const BigNumberReplyCode = "(".charCodeAt(0); /** Represents the null type which is introduced in RESP3. */ export const NullReplyCode = "_".charCodeAt(0); /** Represents the attribute type which is introduced in RESP3. */ export const AttributeReplyCode = "|".charCodeAt(0); /** Represents the push type which is introduced in RESP3. */ export const PushReplyCode = ">".charCodeAt(0); ================================================ FILE: protocol/shared/types.ts ================================================ import type { ErrorReplyError } from "../../errors.ts"; /** * @see https://redis.io/topics/protocol */ export type RedisValue = string | number | Uint8Array; /** * @description Represents the type of the value returned by `SimpleStringReply#value()`. */ export type SimpleString = string; /** * @description Represents the type of the value returned by `IntegerReply#value()`. */ export type Integer = number; /** * @description Represents the type of the value returned by `BulkReply#value()`. */ export type Bulk = BulkString | BulkNil; /** * @description Represents the **bulk string** type in the RESP2 protocol. */ export type BulkString = string; /** * @description Represents the **null bulk string** and **null array** in the RESP2 protocol. */ export type BulkNil = null; /** * @description Represents the some type in the RESP2 protocol. */ export type Raw = SimpleString | Integer | Bulk | ConditionalArray | Binary; export type Binary = Uint8Array; /** * @description Represents the type of the value returned by `ArrayReply#value()`. */ export type ConditionalArray = Raw[]; export type RedisReply = Raw | ConditionalArray; export type RawOrError = Raw | ErrorReplyError; export const okReply = "OK"; export type Protover = 2 | 3; ================================================ FILE: protocol/web_streams/command.ts ================================================ import { readReply } from "./reply.ts"; import { ErrorReplyError } from "../../errors.ts"; import type { BufferedReadableStream } from "../../internal/buffered_readable_stream.ts"; import type { RedisReply, RedisValue } from "../shared/types.ts"; import { encodeCommand, encodeCommands } from "../shared/command.ts"; export async function writeCommand( writable: WritableStream, command: string, args: RedisValue[], ) { const request = encodeCommand(command, args); const writer = writable.getWriter(); try { await writer.write(request); } finally { writer.releaseLock(); } } export async function sendCommand( writable: WritableStream, readable: BufferedReadableStream, command: string, args: RedisValue[], returnUint8Arrays?: boolean, ): Promise { await writeCommand(writable, command, args); return readReply(readable, returnUint8Arrays); } export interface Command { command: string; args: RedisValue[]; returnUint8Arrays?: boolean; } export async function sendCommands( writable: WritableStream, readable: BufferedReadableStream, commands: Command[], ): Promise<(RedisReply | ErrorReplyError)[]> { const request = encodeCommands(commands); const writer = writable.getWriter(); try { await writer.write(request); } finally { writer.releaseLock(); } const ret: (RedisReply | ErrorReplyError)[] = []; for (let i = 0; i < commands.length; i++) { try { const rep = await readReply(readable, commands[i].returnUint8Arrays); ret.push(rep); } catch (e) { if (e instanceof ErrorReplyError) { ret.push(e); } else { throw e; } } } return ret; } ================================================ FILE: protocol/web_streams/mod.ts ================================================ import { sendCommand, sendCommands, writeCommand } from "./command.ts"; import { readReply } from "./reply.ts"; import type { Command, Protocol as BaseProtocol } from "../shared/protocol.ts"; import type { RedisReply, RedisValue } from "../shared/types.ts"; import type { ErrorReplyError } from "../../errors.ts"; import { BufferedReadableStream } from "../../internal/buffered_readable_stream.ts"; export class Protocol implements BaseProtocol { #readable: BufferedReadableStream; #writable: WritableStream; constructor(conn: Deno.Conn) { this.#readable = new BufferedReadableStream(conn.readable); this.#writable = conn.writable; } sendCommand( command: string, args: RedisValue[], returnsUint8Arrays?: boolean | undefined, ): Promise { return sendCommand( this.#writable, this.#readable, command, args, returnsUint8Arrays, ); } readReply(returnsUint8Arrays?: boolean): Promise { return readReply(this.#readable, returnsUint8Arrays); } writeCommand(command: Command): Promise { return writeCommand(this.#writable, command.command, command.args); } pipeline(commands: Command[]): Promise> { return sendCommands(this.#writable, this.#readable, commands); } } ================================================ FILE: protocol/web_streams/reply.ts ================================================ import type * as types from "../shared/types.ts"; import { ArrayReplyCode, AttributeReplyCode, BigNumberReplyCode, BlobErrorReplyCode, BooleanReplyCode, BulkReplyCode, DoubleReplyCode, ErrorReplyCode, IntegerReplyCode, MapReplyCode, NullReplyCode, PushReplyCode, SetReplyCode, SimpleStringCode, VerbatimStringCode, } from "../shared/reply.ts"; import { ErrorReplyError, NotImplementedError } from "../../errors.ts"; import { decoder } from "../../internal/encoding.ts"; import type { BufferedReadableStream } from "../../internal/buffered_readable_stream.ts"; export async function readReply( readable: BufferedReadableStream, returnUint8Arrays?: boolean, ) { const line = await readable.readLine(); const code = line[0]; switch (code) { case ErrorReplyCode: { throw new ErrorReplyError(decoder.decode(line)); } case IntegerReplyCode: { return Number.parseInt(decoder.decode(line.subarray(1))); } case SimpleStringCode: { const body = line.slice(1, -2); return returnUint8Arrays ? body : decoder.decode(body); } case BulkReplyCode: case VerbatimStringCode: { const size = Number.parseInt(decoder.decode(line.subarray(1))); if (size < 0) { // nil bulk reply return null; } const buf = await readable.readN(size + 2); const body = buf.subarray(0, size); // Strip CR and LF. return returnUint8Arrays ? body : decoder.decode(body); } case BlobErrorReplyCode: { const size = Number.parseInt(decoder.decode(line.subarray(1))); const buf = await readable.readN(size + 2); const body = buf.subarray(0, size); // Strip CR and LF. throw new ErrorReplyError(decoder.decode(body)); } case ArrayReplyCode: case PushReplyCode: { const size = Number.parseInt(decoder.decode(line.slice(1))); if (size === -1) { // `-1` indicates a null array return null; } const array: Array = []; for (let i = 0; i < size; i++) { array.push(await readReply(readable, returnUint8Arrays)); } return array; } case MapReplyCode: { // NOTE: We treat a map type as an array to keep backward compatibility. const numberOfFieldValuePairs = Number.parseInt( decoder.decode(line.slice(1)), ); if (numberOfFieldValuePairs === -1) { return null; } const entries: Array = []; for (let i = 0; i < (numberOfFieldValuePairs * 2); i++) { entries.push(await readReply(readable, returnUint8Arrays)); } return entries; } case SetReplyCode: { // NOTE: We treat a set type as an array to keep backward compatibility. const size = Number.parseInt(decoder.decode(line.slice(1))); if (size === -1) { // `-1` indicates a null set return null; } const set: Array = []; for (let i = 0; i < size; i++) { set.push(await readReply(readable, returnUint8Arrays)); } return set; } case BooleanReplyCode: { const isTrue = line[1] === 116; return isTrue ? 1 // `#t` : 0; // `#f` } case BigNumberReplyCode: case DoubleReplyCode: { const body = line.subarray(1, -2); return returnUint8Arrays ? body : decoder.decode(body); } case NullReplyCode: { return null; } case AttributeReplyCode: { // NOTE: Currently, we simply drop attributes. // TODO: Provide a way for users to capture attributes. const numberOfAttributes = Number.parseInt( decoder.decode(line.slice(1)), ); if (numberOfAttributes === -1) { return readReply(readable, returnUint8Arrays); // Reads the next reply. } for (let i = 0; i < numberOfAttributes; i++) { await readReply(readable, returnUint8Arrays); // Reads a key await readReply(readable, returnUint8Arrays); // Reads a value } return readReply(readable, returnUint8Arrays); // Reads the next reply. } default: throw new NotImplementedError( `'${String.fromCharCode(code)}' reply is not implemented`, ); } } ================================================ FILE: protocol/web_streams/reply_test.ts ================================================ import { assertEquals, assertRejects } from "../../deps/std/assert.ts"; import { readReply } from "./reply.ts"; import { BufferedReadableStream } from "../../internal/buffered_readable_stream.ts"; import { ErrorReplyError } from "../../errors.ts"; import type { RedisReply } from "../shared/types.ts"; Deno.test({ name: "readReply", permissions: "none", fn: async (t) => { await t.step("blob", async () => { const readable = createReadableByteStream("$12\r\nhello\nworld!\r\n"); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, "hello\nworld!"); }); await t.step("simple string", async () => { const readable = createReadableByteStream("+OK\r\n"); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, "OK"); }); await t.step("integer", async () => { const readable = createReadableByteStream(":1234\r\n"); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, 1234); }); await t.step("array", async () => { const readable = createReadableByteStream( "*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", ); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, ["foo", [456, "OK"], null, "123456", 78]); }); await t.step("map", async () => { const readable = createReadableByteStream( "%2\r\n+foo\r\n:1\r\n+bar\r\n:2\r\n", ); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, ["foo", 1, "bar", 2]); }); await t.step("set", async () => { const readable = createReadableByteStream( "~3\r\n+foo\r\n:5\r\n$3\r\nbar\r\n", ); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, ["foo", 5, "bar"]); }); await t.step("null", async () => { const readable = createReadableByteStream( "_\r\n", ); const reply = await readReply(new BufferedReadableStream(readable)); assertEquals(reply, null); }); await t.step("blob error", async () => { const readable = createReadableByteStream( "!21\r\nSYNTAX invalid syntax\r\n_\r\n", ); const buf = new BufferedReadableStream(readable); await assertRejects( () => readReply(buf), ErrorReplyError, "SYNTAX invalid syntax", ); assertEquals(await readReply(buf), null); }); await t.step("attribute", async () => { const cases: Array<[given: string, expected: RedisReply]> = [ [ "|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", "Hello World", ], [ "*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", [123, "str", "1.23"], ], ]; for ( const [given, expected] of cases ) { const readable = createReadableByteStream(given); const buf = new BufferedReadableStream(readable); const actual = await readReply(buf); assertEquals(actual, expected); } }); }, }); function createReadableByteStream(payload: string): ReadableStream { const encoder = new TextEncoder(); let numRead = 0; return new ReadableStream({ type: "bytes", pull(controller) { if (controller.byobRequest?.view) { const view = controller.byobRequest.view; const buf = new Uint8Array( view.buffer, view.byteOffset, view.byteLength, ); const remaining = payload.length - numRead; const written = Math.min(buf.byteLength, remaining); buf.set(encoder.encode(payload.slice(numRead, numRead + written))); numRead += written; controller.byobRequest.respond(written); } else { controller.enqueue(encoder.encode(payload[numRead])); numRead++; } if (numRead >= payload.length) { controller.close(); } }, }); } ================================================ FILE: redis.ts ================================================ import type { ACLLogMode, BitfieldOpts, BitfieldWithOverflowOpts, ClientCachingMode, ClientKillOpts, ClientListOpts, ClientPauseMode, ClientTrackingOpts, ClientUnblockingBehaviour, ClusterFailoverMode, ClusterResetMode, ClusterSetSlotSubcommand, GeoRadiusOpts, GeoUnit, HelloOpts, HScanOpts, LInsertLocation, LPosOpts, LPosWithCountOpts, MemoryUsageOpts, MigrateOpts, RedisCommands, RestoreOpts, ScanOpts, ScriptDebugMode, SetOpts, SetReply, SetWithModeOpts, ShutdownMode, SortOpts, SortWithDestinationOpts, SScanOpts, StralgoAlgorithm, StralgoOpts, StralgoTarget, ZAddOpts, ZAddReply, ZInterOpts, ZInterstoreOpts, ZRangeByLexOpts, ZRangeByScoreOpts, ZRangeOpts, ZScanOpts, ZUnionstoreOpts, } from "./command.ts"; import { createRedisConnection } from "./default_connection.ts"; import type { Connection, SendCommandOptions } from "./connection.ts"; import type { RedisConnectionOptions } from "./connection.ts"; import type { Client } from "./client.ts"; import type { RedisSubscription } from "./subscription.ts"; import { createDefaultClient } from "./default_client.ts"; import type { ConnectionEventMap, ConnectionEventType } from "./events.ts"; import type { TypedEventTarget } from "./internal/typed_event_target.ts"; import type { Binary, Bulk, BulkNil, BulkString, ConditionalArray, Integer, Raw, RedisReply, RedisValue, SimpleString, } from "./protocol/shared/types.ts"; import { createRedisPipeline } from "./pipeline.ts"; import type { StartEndCount, XAddFieldValues, XClaimJustXId, XClaimMessages, XClaimOpts, XId, XIdAdd, XIdInput, XIdNeg, XIdPos, XKeyId, XKeyIdGroup, XKeyIdGroupLike, XKeyIdLike, XMaxlen, XReadGroupOpts, XReadIdData, XReadOpts, XReadStreamRaw, } from "./stream.ts"; import { convertMap, isCondArray, isNumber, isString, parseXGroupDetail, parseXId, parseXMessage, parseXPendingConsumers, parseXPendingCounts, parseXReadReply, rawnum, rawstr, xidstr, } from "./stream.ts"; const binaryCommandOptions = { returnUint8Arrays: true, }; /** * A high-level client for Redis. */ export interface Redis extends RedisCommands, TypedEventTarget { readonly isClosed: boolean; readonly isConnected: boolean; /** * Low level interface for Redis server */ sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ): Promise; connect(): Promise; close(): void; [Symbol.dispose](): void; } class RedisImpl implements Redis { private readonly client: Client; get isClosed() { return this.client.connection.isClosed; } get isConnected() { return this.client.connection.isConnected; } constructor(client: Client) { this.client = client; } addEventListener( type: K, callback: (event: CustomEvent) => void, options?: boolean | AddEventListenerOptions, ): void { return this.client.connection.addEventListener( type, callback, options, ); } removeEventListener( type: K, callback: (event: CustomEvent) => void, options?: boolean | EventListenerOptions, ): void { return this.client.connection.removeEventListener( type, callback, options, ); } sendCommand( command: string, args?: RedisValue[], options?: SendCommandOptions, ) { return this.client.sendCommand(command, args, options); } connect(): Promise { return this.client.connection.connect(); } close(): void { return this.client.close(); } [Symbol.dispose](): void { return this.close(); } async execReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec( command, ...args, ); return reply as T; } async execStatusReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as SimpleString; } async execIntegerReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as Integer; } async execBinaryReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.sendCommand( command, args, binaryCommandOptions, ); return reply as Binary | BulkNil; } async execBulkReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as T; } async execArrayReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as Array; } async execIntegerOrNilReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as Integer | BulkNil; } async execStatusOrNilReply( command: string, ...args: RedisValue[] ): Promise { const reply = await this.client.exec(command, ...args); return reply as SimpleString | BulkNil; } aclCat(categoryname?: string) { if (categoryname !== undefined) { return this.execArrayReply("ACL", "CAT", categoryname); } return this.execArrayReply("ACL", "CAT"); } aclDelUser(...usernames: string[]) { return this.execIntegerReply("ACL", "DELUSER", ...usernames); } aclGenPass(bits?: number) { if (bits !== undefined) { return this.execBulkReply("ACL", "GENPASS", bits); } return this.execBulkReply("ACL", "GENPASS"); } aclGetUser(username: string) { return this.execArrayReply( "ACL", "GETUSER", username, ); } aclHelp() { return this.execArrayReply("ACL", "HELP"); } aclList() { return this.execArrayReply("ACL", "LIST"); } aclLoad() { return this.execStatusReply("ACL", "LOAD"); } aclLog(count: number): Promise; aclLog(mode: ACLLogMode): Promise; aclLog(param: number | ACLLogMode) { if (param === "RESET") { return this.execStatusReply("ACL", "LOG", "RESET"); } return this.execArrayReply("ACL", "LOG", param); } aclSave() { return this.execStatusReply("ACL", "SAVE"); } aclSetUser(username: string, ...rules: string[]) { return this.execStatusReply("ACL", "SETUSER", username, ...rules); } aclUsers() { return this.execArrayReply("ACL", "USERS"); } aclWhoami() { return this.execBulkReply("ACL", "WHOAMI"); } append(key: string, value: RedisValue) { return this.execIntegerReply("APPEND", key, value); } auth(param1: RedisValue, param2?: RedisValue) { if (param2 !== undefined) { return this.execStatusReply("AUTH", param1, param2); } return this.execStatusReply("AUTH", param1); } bgrewriteaof() { return this.execStatusReply("BGREWRITEAOF"); } bgsave() { return this.execStatusReply("BGSAVE"); } bitcount(key: string, start?: number, end?: number) { if (start !== undefined && end !== undefined) { return this.execIntegerReply("BITCOUNT", key, start, end); } return this.execIntegerReply("BITCOUNT", key); } bitfield( key: string, opts?: BitfieldOpts | BitfieldWithOverflowOpts, ) { const args: (number | string)[] = [key]; if (opts?.get) { const { type, offset } = opts.get; args.push("GET", type, offset); } if (opts?.set) { const { type, offset, value } = opts.set; args.push("SET", type, offset, value); } if (opts?.incrby) { const { type, offset, increment } = opts.incrby; args.push("INCRBY", type, offset, increment); } if ((opts as BitfieldWithOverflowOpts)?.overflow) { args.push("OVERFLOW", (opts as BitfieldWithOverflowOpts).overflow); } return this.execArrayReply("BITFIELD", ...args); } bitop(operation: string, destkey: string, ...keys: string[]) { return this.execIntegerReply("BITOP", operation, destkey, ...keys); } bitpos(key: string, bit: number, start?: number, end?: number) { if (start !== undefined && end !== undefined) { return this.execIntegerReply("BITPOS", key, bit, start, end); } if (start !== undefined) { return this.execIntegerReply("BITPOS", key, bit, start); } return this.execIntegerReply("BITPOS", key, bit); } blpop(timeout: number, ...keys: string[]) { return this.execArrayReply("BLPOP", ...keys, timeout) as Promise< [BulkString, BulkString] | BulkNil >; } brpop(timeout: number, ...keys: string[]) { return this.execArrayReply("BRPOP", ...keys, timeout) as Promise< [BulkString, BulkString] | BulkNil >; } brpoplpush(source: string, destination: string, timeout: number) { return this.execBulkReply("BRPOPLPUSH", source, destination, timeout); } bzpopmin(timeout: number, ...keys: string[]) { return this.execArrayReply("BZPOPMIN", ...keys, timeout) as Promise< [BulkString, BulkString, BulkString] | BulkNil >; } bzpopmax(timeout: number, ...keys: string[]) { return this.execArrayReply("BZPOPMAX", ...keys, timeout) as Promise< [BulkString, BulkString, BulkString] | BulkNil >; } clientCaching(mode: ClientCachingMode) { return this.execStatusReply("CLIENT", "CACHING", mode); } clientGetName() { return this.execBulkReply("CLIENT", "GETNAME"); } clientGetRedir() { return this.execIntegerReply("CLIENT", "GETREDIR"); } clientID() { return this.execIntegerReply("CLIENT", "ID"); } clientInfo() { return this.execBulkReply("CLIENT", "INFO"); } clientKill(opts: ClientKillOpts) { const args: (string | number)[] = []; if (opts.addr) { args.push("ADDR", opts.addr); } if (opts.laddr) { args.push("LADDR", opts.laddr); } if (opts.id) { args.push("ID", opts.id); } if (opts.type) { args.push("TYPE", opts.type); } if (opts.user) { args.push("USER", opts.user); } if (opts.skipme) { args.push("SKIPME", opts.skipme); } return this.execIntegerReply("CLIENT", "KILL", ...args); } clientList(opts?: ClientListOpts) { if (opts && opts.type && opts.ids) { throw new Error("only one of `type` or `ids` can be specified"); } if (opts && opts.type) { return this.execBulkReply("CLIENT", "LIST", "TYPE", opts.type); } if (opts && opts.ids) { return this.execBulkReply("CLIENT", "LIST", "ID", ...opts.ids); } return this.execBulkReply("CLIENT", "LIST"); } clientPause(timeout: number, mode?: ClientPauseMode) { if (mode) { return this.execStatusReply("CLIENT", "PAUSE", timeout, mode); } return this.execStatusReply("CLIENT", "PAUSE", timeout); } clientSetName(connectionName: string) { return this.execStatusReply("CLIENT", "SETNAME", connectionName); } clientTracking(opts: ClientTrackingOpts) { const args: (number | string)[] = [opts.mode]; if (opts.redirect) { args.push("REDIRECT", opts.redirect); } if (opts.prefixes) { opts.prefixes.forEach((prefix) => { args.push("PREFIX"); args.push(prefix); }); } if (opts.bcast) { args.push("BCAST"); } if (opts.optIn) { args.push("OPTIN"); } if (opts.optOut) { args.push("OPTOUT"); } if (opts.noLoop) { args.push("NOLOOP"); } return this.execStatusReply("CLIENT", "TRACKING", ...args); } clientTrackingInfo() { return this.execArrayReply("CLIENT", "TRACKINGINFO"); } clientUnblock( id: number, behaviour?: ClientUnblockingBehaviour, ): Promise { if (behaviour) { return this.execIntegerReply("CLIENT", "UNBLOCK", id, behaviour); } return this.execIntegerReply("CLIENT", "UNBLOCK", id); } clientUnpause(): Promise { return this.execStatusReply("CLIENT", "UNPAUSE"); } asking() { return this.execStatusReply("ASKING"); } clusterAddSlots(...slots: number[]) { return this.execStatusReply("CLUSTER", "ADDSLOTS", ...slots); } clusterCountFailureReports(nodeId: string) { return this.execIntegerReply("CLUSTER", "COUNT-FAILURE-REPORTS", nodeId); } clusterCountKeysInSlot(slot: number) { return this.execIntegerReply("CLUSTER", "COUNTKEYSINSLOT", slot); } clusterDelSlots(...slots: number[]) { return this.execStatusReply("CLUSTER", "DELSLOTS", ...slots); } clusterFailover(mode?: ClusterFailoverMode) { if (mode) { return this.execStatusReply("CLUSTER", "FAILOVER", mode); } return this.execStatusReply("CLUSTER", "FAILOVER"); } clusterFlushSlots() { return this.execStatusReply("CLUSTER", "FLUSHSLOTS"); } clusterForget(nodeId: string) { return this.execStatusReply("CLUSTER", "FORGET", nodeId); } clusterGetKeysInSlot(slot: number, count: number) { return this.execArrayReply( "CLUSTER", "GETKEYSINSLOT", slot, count, ); } clusterInfo() { return this.execStatusReply("CLUSTER", "INFO"); } clusterKeySlot(key: string) { return this.execIntegerReply("CLUSTER", "KEYSLOT", key); } clusterMeet(ip: string, port: number) { return this.execStatusReply("CLUSTER", "MEET", ip, port); } clusterMyID() { return this.execStatusReply("CLUSTER", "MYID"); } clusterNodes() { return this.execBulkReply("CLUSTER", "NODES"); } clusterReplicas(nodeId: string) { return this.execArrayReply("CLUSTER", "REPLICAS", nodeId); } clusterReplicate(nodeId: string) { return this.execStatusReply("CLUSTER", "REPLICATE", nodeId); } clusterReset(mode?: ClusterResetMode) { if (mode) { return this.execStatusReply("CLUSTER", "RESET", mode); } return this.execStatusReply("CLUSTER", "RESET"); } clusterSaveConfig() { return this.execStatusReply("CLUSTER", "SAVECONFIG"); } clusterSetSlot( slot: number, subcommand: ClusterSetSlotSubcommand, nodeId?: string, ) { if (nodeId !== undefined) { return this.execStatusReply( "CLUSTER", "SETSLOT", slot, subcommand, nodeId, ); } return this.execStatusReply("CLUSTER", "SETSLOT", slot, subcommand); } clusterSlaves(nodeId: string) { return this.execArrayReply("CLUSTER", "SLAVES", nodeId); } clusterSlots() { return this.execArrayReply("CLUSTER", "SLOTS"); } command() { return this.execArrayReply("COMMAND") as Promise< [BulkString, Integer, BulkString[], Integer, Integer, Integer][] >; } commandCount() { return this.execIntegerReply("COMMAND", "COUNT"); } commandGetKeys() { return this.execArrayReply("COMMAND", "GETKEYS"); } commandInfo(...commandNames: string[]) { return this.execArrayReply("COMMAND", "INFO", ...commandNames) as Promise< ( | [BulkString, Integer, BulkString[], Integer, Integer, Integer] | BulkNil )[] >; } configGet(parameter: string) { return this.execArrayReply("CONFIG", "GET", parameter); } configResetStat() { return this.execStatusReply("CONFIG", "RESETSTAT"); } configRewrite() { return this.execStatusReply("CONFIG", "REWRITE"); } configSet(parameter: string, value: string | number) { return this.execStatusReply("CONFIG", "SET", parameter, value); } dbsize() { return this.execIntegerReply("DBSIZE"); } debugObject(key: string) { return this.execStatusReply("DEBUG", "OBJECT", key); } debugSegfault() { return this.execStatusReply("DEBUG", "SEGFAULT"); } decr(key: string) { return this.execIntegerReply("DECR", key); } decrby(key: string, decrement: number) { return this.execIntegerReply("DECRBY", key, decrement); } del(...keys: string[]) { return this.execIntegerReply("DEL", ...keys); } discard() { return this.execStatusReply("DISCARD"); } dump(key: string) { return this.execBinaryReply("DUMP", key); } echo(message: RedisValue) { return this.execBulkReply("ECHO", message); } eval(script: string, keys: string[], args: string[]) { return this.execReply( "EVAL", script, keys.length, ...keys, ...args, ); } evalsha(sha1: string, keys: string[], args: string[]) { return this.execReply( "EVALSHA", sha1, keys.length, ...keys, ...args, ); } exec() { return this.execArrayReply("EXEC"); } exists(...keys: string[]) { return this.execIntegerReply("EXISTS", ...keys); } expire(key: string, seconds: number) { return this.execIntegerReply("EXPIRE", key, seconds); } expireat(key: string, timestamp: string) { return this.execIntegerReply("EXPIREAT", key, timestamp); } flushall(async?: boolean) { if (async) { return this.execStatusReply("FLUSHALL", "ASYNC"); } return this.execStatusReply("FLUSHALL"); } flushdb(async?: boolean) { if (async) { return this.execStatusReply("FLUSHDB", "ASYNC"); } return this.execStatusReply("FLUSHDB"); } // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any` geoadd(key: string, ...params: any[]) { const args: (string | number)[] = [key]; if (Array.isArray(params[0])) { args.push(...params.flatMap((e) => e)); } else if (typeof params[0] === "object") { for (const [member, lnglat] of Object.entries(params[0])) { args.push(...(lnglat as [number, number]), member); } } else { args.push(...params); } return this.execIntegerReply("GEOADD", ...args); } geohash(key: string, ...members: string[]) { return this.execArrayReply("GEOHASH", key, ...members); } geopos(key: string, ...members: string[]) { return this.execArrayReply("GEOPOS", key, ...members) as Promise< ([BulkString, BulkString] | BulkNil | [])[] >; } geodist( key: string, member1: string, member2: string, unit?: GeoUnit, ) { if (unit) { return this.execBulkReply("GEODIST", key, member1, member2, unit); } return this.execBulkReply("GEODIST", key, member1, member2); } georadius( key: string, longitude: number, latitude: number, radius: number, unit: "m" | "km" | "ft" | "mi", opts?: GeoRadiusOpts, ) { const args = this.pushGeoRadiusOpts( [key, longitude, latitude, radius, unit], opts, ); return this.execArrayReply("GEORADIUS", ...args); } georadiusbymember( key: string, member: string, radius: number, unit: GeoUnit, opts?: GeoRadiusOpts, ) { const args = this.pushGeoRadiusOpts([key, member, radius, unit], opts); return this.execArrayReply("GEORADIUSBYMEMBER", ...args); } private pushGeoRadiusOpts( args: (string | number)[], opts?: GeoRadiusOpts, ) { if (opts?.withCoord) { args.push("WITHCOORD"); } if (opts?.withDist) { args.push("WITHDIST"); } if (opts?.withHash) { args.push("WITHHASH"); } if (opts?.count !== undefined) { args.push(opts.count); } if (opts?.sort) { args.push(opts.sort); } if (opts?.store !== undefined) { args.push(opts.store); } if (opts?.storeDist !== undefined) { args.push(opts.storeDist); } return args; } get(key: string) { return this.execBulkReply("GET", key); } getbit(key: string, offset: number) { return this.execIntegerReply("GETBIT", key, offset); } getrange(key: string, start: number, end: number) { return this.execBulkReply("GETRANGE", key, start, end); } getset(key: string, value: RedisValue) { return this.execBulkReply("GETSET", key, value); } hdel(key: string, ...fields: string[]) { return this.execIntegerReply("HDEL", key, ...fields); } hexists(key: string, field: string) { return this.execIntegerReply("HEXISTS", key, field); } hget(key: string, field: string) { return this.execBulkReply("HGET", key, field); } hgetall(key: string) { return this.execArrayReply("HGETALL", key); } hincrby(key: string, field: string, increment: number) { return this.execIntegerReply("HINCRBY", key, field, increment); } hincrbyfloat(key: string, field: string, increment: number) { return this.execBulkReply( "HINCRBYFLOAT", key, field, increment, ); } hkeys(key: string) { return this.execArrayReply("HKEYS", key); } hlen(key: string) { return this.execIntegerReply("HLEN", key); } hmget(key: string, ...fields: string[]) { return this.execArrayReply("HMGET", key, ...fields); } // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any` hmset(key: string, ...params: any[]) { const args = [key] as RedisValue[]; if (Array.isArray(params[0])) { args.push(...params.flatMap((e) => e)); } else if (typeof params[0] === "object") { for (const [field, value] of Object.entries(params[0])) { args.push(field, value as RedisValue); } } else { args.push(...params); } return this.execStatusReply("HMSET", ...args); } // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any` hset(key: string, ...params: any[]) { const args = [key] as RedisValue[]; if (Array.isArray(params[0])) { args.push(...params.flatMap((e) => e)); } else if (typeof params[0] === "object") { for (const [field, value] of Object.entries(params[0])) { args.push(field, value as RedisValue); } } else { args.push(...params); } return this.execIntegerReply("HSET", ...args); } hsetnx(key: string, field: string, value: RedisValue) { return this.execIntegerReply("HSETNX", key, field, value); } hstrlen(key: string, field: string) { return this.execIntegerReply("HSTRLEN", key, field); } hvals(key: string) { return this.execArrayReply("HVALS", key); } incr(key: string) { return this.execIntegerReply("INCR", key); } incrby(key: string, increment: number) { return this.execIntegerReply("INCRBY", key, increment); } incrbyfloat(key: string, increment: number) { return this.execBulkReply("INCRBYFLOAT", key, increment); } info(section?: string) { if (section !== undefined) { return this.execStatusReply("INFO", section); } return this.execStatusReply("INFO"); } keys(pattern: string) { return this.execArrayReply("KEYS", pattern); } lastsave() { return this.execIntegerReply("LASTSAVE"); } lindex(key: string, index: number) { return this.execBulkReply("LINDEX", key, index); } linsert(key: string, loc: LInsertLocation, pivot: string, value: RedisValue) { return this.execIntegerReply("LINSERT", key, loc, pivot, value); } llen(key: string) { return this.execIntegerReply("LLEN", key); } lpop(key: string): Promise; lpop(key: string, count: number): Promise>; lpop(key: string, count?: number): Promise> { if (count == null) { return this.execBulkReply("LPOP", key); } else { return this.execArrayReply("LPOP", key, count); } } lpos( key: string, element: RedisValue, opts?: LPosOpts, ): Promise; lpos( key: string, element: RedisValue, opts: LPosWithCountOpts, ): Promise; lpos( key: string, element: RedisValue, opts?: LPosOpts | LPosWithCountOpts, ): Promise { const args = [element]; if (opts?.rank != null) { args.push("RANK", String(opts.rank)); } if (opts?.count != null) { args.push("COUNT", String(opts.count)); } if (opts?.maxlen != null) { args.push("MAXLEN", String(opts.maxlen)); } return opts?.count == null ? this.execIntegerReply("LPOS", key, ...args) : this.execArrayReply("LPOS", key, ...args); } lpush(key: string, ...elements: RedisValue[]) { return this.execIntegerReply("LPUSH", key, ...elements); } lpushx(key: string, ...elements: RedisValue[]) { return this.execIntegerReply("LPUSHX", key, ...elements); } lrange(key: string, start: number, stop: number) { return this.execArrayReply("LRANGE", key, start, stop); } lrem(key: string, count: number, element: string | number) { return this.execIntegerReply("LREM", key, count, element); } lset(key: string, index: number, element: string | number) { return this.execStatusReply("LSET", key, index, element); } ltrim(key: string, start: number, stop: number) { return this.execStatusReply("LTRIM", key, start, stop); } memoryDoctor() { return this.execBulkReply("MEMORY", "DOCTOR"); } memoryHelp() { return this.execArrayReply("MEMORY", "HELP"); } memoryMallocStats() { return this.execBulkReply("MEMORY", "MALLOC", "STATS"); } memoryPurge() { return this.execStatusReply("MEMORY", "PURGE"); } memoryStats() { return this.execArrayReply("MEMORY", "STATS"); } memoryUsage(key: string, opts?: MemoryUsageOpts) { const args: (number | string)[] = [key]; if (opts?.samples !== undefined) { args.push("SAMPLES", opts.samples); } return this.execIntegerReply("MEMORY", "USAGE", ...args); } mget(...keys: string[]) { return this.execArrayReply("MGET", ...keys); } migrate( host: string, port: number, key: string, destinationDB: string, timeout: number, opts?: MigrateOpts, ) { const args = [host, port, key, destinationDB, timeout]; if (opts?.copy) { args.push("COPY"); } if (opts?.replace) { args.push("REPLACE"); } if (opts?.auth !== undefined) { args.push("AUTH", opts.auth); } if (opts?.keys) { args.push("KEYS", ...opts.keys); } return this.execStatusReply("MIGRATE", ...args); } moduleList() { return this.execArrayReply("MODULE", "LIST"); } moduleLoad(path: string, ...args: string[]) { return this.execStatusReply("MODULE", "LOAD", path, ...args); } moduleUnload(name: string) { return this.execStatusReply("MODULE", "UNLOAD", name); } monitor() { throw new Error("not supported yet"); } move(key: string, db: string) { return this.execIntegerReply("MOVE", key, db); } // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any` mset(...params: any[]) { const args: RedisValue[] = []; if (Array.isArray(params[0])) { args.push(...params.flatMap((e) => e)); } else if (typeof params[0] === "object") { for (const [key, value] of Object.entries(params[0])) { args.push(key, value as RedisValue); } } else { args.push(...params); } return this.execStatusReply("MSET", ...args); } // deno-lint-ignore no-explicit-any -- TODO: improve the signature not to use `any` msetnx(...params: any[]) { const args: RedisValue[] = []; if (Array.isArray(params[0])) { args.push(...params.flatMap((e) => e)); } else if (typeof params[0] === "object") { for (const [key, value] of Object.entries(params[0])) { args.push(key, value as RedisValue); } } else { args.push(...params); } return this.execIntegerReply("MSETNX", ...args); } multi() { return this.execStatusReply("MULTI"); } objectEncoding(key: string) { return this.execBulkReply("OBJECT", "ENCODING", key); } objectFreq(key: string) { return this.execIntegerOrNilReply("OBJECT", "FREQ", key); } objectHelp() { return this.execArrayReply("OBJECT", "HELP"); } objectIdletime(key: string) { return this.execIntegerOrNilReply("OBJECT", "IDLETIME", key); } objectRefCount(key: string) { return this.execIntegerOrNilReply("OBJECT", "REFCOUNT", key); } persist(key: string) { return this.execIntegerReply("PERSIST", key); } pexpire(key: string, milliseconds: number) { return this.execIntegerReply("PEXPIRE", key, milliseconds); } pexpireat(key: string, millisecondsTimestamp: number) { return this.execIntegerReply("PEXPIREAT", key, millisecondsTimestamp); } pfadd(key: string, ...elements: string[]) { return this.execIntegerReply("PFADD", key, ...elements); } pfcount(...keys: string[]) { return this.execIntegerReply("PFCOUNT", ...keys); } pfmerge(destkey: string, ...sourcekeys: string[]) { return this.execStatusReply("PFMERGE", destkey, ...sourcekeys); } ping(message?: RedisValue) { if (message) { return this.execBulkReply("PING", message); } return this.execStatusReply("PING"); } psetex(key: string, milliseconds: number, value: RedisValue) { return this.execStatusReply("PSETEX", key, milliseconds, value); } publish(channel: string, message: string) { return this.execIntegerReply("PUBLISH", channel, message); } // deno-lint-ignore no-explicit-any -- This is a private property. #subscription?: RedisSubscription; async subscribe( ...channels: string[] ) { if (this.#subscription) { await this.#subscription.subscribe(...channels); return this.#subscription; } const subscription = await this.client.subscribe( "SUBSCRIBE", ...channels, ); this.#subscription = subscription; return subscription; } async psubscribe( ...patterns: string[] ) { if (this.#subscription) { await this.#subscription.psubscribe(...patterns); return this.#subscription; } const subscription = await this.client.subscribe( "PSUBSCRIBE", ...patterns, ); this.#subscription = subscription; return subscription; } pubsubChannels(pattern?: string) { if (pattern !== undefined) { return this.execArrayReply("PUBSUB", "CHANNELS", pattern); } return this.execArrayReply("PUBSUB", "CHANNELS"); } pubsubNumpat() { return this.execIntegerReply("PUBSUB", "NUMPAT"); } pubsubNumsub(...channels: string[]) { return this.execArrayReply( "PUBSUB", "NUMSUB", ...channels, ); } pttl(key: string) { return this.execIntegerReply("PTTL", key); } quit() { return this.execStatusReply("QUIT").finally(() => this.close()); } randomkey() { return this.execBulkReply("RANDOMKEY"); } readonly() { return this.execStatusReply("READONLY"); } readwrite() { return this.execStatusReply("READWRITE"); } rename(key: string, newkey: string) { return this.execStatusReply("RENAME", key, newkey); } renamenx(key: string, newkey: string) { return this.execIntegerReply("RENAMENX", key, newkey); } restore( key: string, ttl: number, serializedValue: Binary, opts?: RestoreOpts, ) { const args = [key, ttl, serializedValue]; if (opts?.replace) { args.push("REPLACE"); } if (opts?.absttl) { args.push("ABSTTL"); } if (opts?.idletime !== undefined) { args.push("IDLETIME", opts.idletime); } if (opts?.freq !== undefined) { args.push("FREQ", opts.freq); } return this.execStatusReply("RESTORE", ...args); } role() { return this.execArrayReply("ROLE") as Promise< | ["master", Integer, BulkString[][]] | ["slave", BulkString, Integer, BulkString, Integer] | ["sentinel", BulkString[]] >; } rpop(key: string) { return this.execBulkReply("RPOP", key); } rpoplpush(source: string, destination: string) { return this.execBulkReply("RPOPLPUSH", source, destination); } rpush(key: string, ...elements: RedisValue[]) { return this.execIntegerReply("RPUSH", key, ...elements); } rpushx(key: string, ...elements: RedisValue[]) { return this.execIntegerReply("RPUSHX", key, ...elements); } sadd(key: string, ...members: string[]) { return this.execIntegerReply("SADD", key, ...members); } save() { return this.execStatusReply("SAVE"); } scard(key: string) { return this.execIntegerReply("SCARD", key); } scriptDebug(mode: ScriptDebugMode) { return this.execStatusReply("SCRIPT", "DEBUG", mode); } scriptExists(...sha1s: string[]) { return this.execArrayReply("SCRIPT", "EXISTS", ...sha1s); } scriptFlush() { return this.execStatusReply("SCRIPT", "FLUSH"); } scriptKill() { return this.execStatusReply("SCRIPT", "KILL"); } scriptLoad(script: string) { return this.execStatusReply("SCRIPT", "LOAD", script); } sdiff(...keys: string[]) { return this.execArrayReply("SDIFF", ...keys); } sdiffstore(destination: string, ...keys: string[]) { return this.execIntegerReply("SDIFFSTORE", destination, ...keys); } select(index: number) { return this.execStatusReply("SELECT", index); } hello(opts?: HelloOpts): Promise { const args: Array = []; if (opts?.protover != null) { args.push(opts.protover); } if (opts?.auth != null) { args.push("AUTH", opts.auth.username, opts.auth.password); } if (opts?.clientName != null) { args.push("SETNAME", opts.clientName); } return this.execArrayReply("HELLO", ...args); } set( key: string, value: RedisValue, opts?: TSetOpts, ): Promise> { const args: RedisValue[] = [key, value]; if (opts?.ex != null) { args.push("EX", opts.ex); } else if (opts?.px != null) { args.push("PX", opts.px); } else if (opts?.exat != null) { args.push("EXAT", opts.exat); } else if (opts?.pxat != null) { args.push("PXAT", opts.pxat); } // TODO: Isn't `KEEPTTL` option exclusive with `EX`, `PX`, etc.? if (opts?.keepttl) { args.push("KEEPTTL"); } let isAbleToReturnNil = false; if (opts?.nx) { args.push("NX"); isAbleToReturnNil = true; } else if (opts?.xx) { args.push("XX"); isAbleToReturnNil = true; } else if ((opts as SetWithModeOpts)?.mode) { args.push((opts as SetWithModeOpts).mode); isAbleToReturnNil = true; } if (opts?.get) { args.push("GET"); } const promise = isAbleToReturnNil ? this.execStatusOrNilReply("SET", ...args) : this.execStatusReply("SET", ...args); return promise as Promise>; } setbit(key: string, offset: number, value: RedisValue) { return this.execIntegerReply("SETBIT", key, offset, value); } setex(key: string, seconds: number, value: RedisValue) { return this.execStatusReply("SETEX", key, seconds, value); } setnx(key: string, value: RedisValue) { return this.execIntegerReply("SETNX", key, value); } setrange(key: string, offset: number, value: RedisValue) { return this.execIntegerReply("SETRANGE", key, offset, value); } shutdown(mode?: ShutdownMode) { if (mode) { return this.execStatusReply("SHUTDOWN", mode); } return this.execStatusReply("SHUTDOWN"); } sinter(...keys: string[]) { return this.execArrayReply("SINTER", ...keys); } sinterstore(destination: string, ...keys: string[]) { return this.execIntegerReply("SINTERSTORE", destination, ...keys); } sismember(key: string, member: string) { return this.execIntegerReply("SISMEMBER", key, member); } slaveof(host: string, port: number) { return this.execStatusReply("SLAVEOF", host, port); } slaveofNoOne() { return this.execStatusReply("SLAVEOF", "NO ONE"); } replicaof(host: string, port: number) { return this.execStatusReply("REPLICAOF", host, port); } replicaofNoOne() { return this.execStatusReply("REPLICAOF", "NO ONE"); } slowlog(subcommand: string, ...args: string[]) { return this.execArrayReply("SLOWLOG", subcommand, ...args); } smembers(key: string) { return this.execArrayReply("SMEMBERS", key); } smove(source: string, destination: string, member: string) { return this.execIntegerReply("SMOVE", source, destination, member); } sort( key: string, opts?: SortOpts, ): Promise; sort( key: string, opts?: SortWithDestinationOpts, ): Promise; sort( key: string, opts?: SortOpts | SortWithDestinationOpts, ) { const args: (number | string)[] = [key]; if (opts?.by !== undefined) { args.push("BY", opts.by); } if (opts?.limit) { args.push("LIMIT", opts.limit.offset, opts.limit.count); } if (opts?.patterns) { args.push(...opts.patterns.flatMap((pattern) => ["GET", pattern])); } if (opts?.order) { args.push(opts.order); } if (opts?.alpha) { args.push("ALPHA"); } if ((opts as SortWithDestinationOpts)?.destination !== undefined) { args.push("STORE", (opts as SortWithDestinationOpts).destination); return this.execIntegerReply("SORT", ...args); } return this.execArrayReply("SORT", ...args); } spop(key: string): Promise; spop(key: string, count: number): Promise; spop(key: string, count?: number) { if (count !== undefined) { return this.execArrayReply("SPOP", key, count); } return this.execBulkReply("SPOP", key); } srandmember(key: string): Promise; srandmember(key: string, count: number): Promise; srandmember(key: string, count?: number) { if (count !== undefined) { return this.execArrayReply("SRANDMEMBER", key, count); } return this.execBulkReply("SRANDMEMBER", key); } srem(key: string, ...members: string[]) { return this.execIntegerReply("SREM", key, ...members); } stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, ): Promise; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { len: true }, ): Promise; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { idx: true }, ): Promise< [ string, //`"matches"` Array<[[number, number], [number, number]]>, string, // `"len"` Integer, ] >; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: { idx: true; withmatchlen: true }, ): Promise< [ string, // `"matches"` Array<[[number, number], [number, number], number]>, string, // `"len"` Integer, ] >; stralgo( algorithm: StralgoAlgorithm, target: StralgoTarget, a: string, b: string, opts?: StralgoOpts, ) { const args: (number | string)[] = []; if (opts?.idx) { args.push("IDX"); } if (opts?.len) { args.push("LEN"); } if (opts?.withmatchlen) { args.push("WITHMATCHLEN"); } if (opts?.minmatchlen) { args.push("MINMATCHLEN"); args.push(opts.minmatchlen); } return this.execReply( "STRALGO", algorithm, target, a, b, ...args, ); } strlen(key: string) { return this.execIntegerReply("STRLEN", key); } sunion(...keys: string[]) { return this.execArrayReply("SUNION", ...keys); } sunionstore(destination: string, ...keys: string[]) { return this.execIntegerReply("SUNIONSTORE", destination, ...keys); } swapdb(index1: number, index2: number) { return this.execStatusReply("SWAPDB", index1, index2); } sync() { throw new Error("not implemented"); } time() { return this.execArrayReply("TIME") as Promise<[BulkString, BulkString]>; } touch(...keys: string[]) { return this.execIntegerReply("TOUCH", ...keys); } ttl(key: string) { return this.execIntegerReply("TTL", key); } type(key: string) { return this.execStatusReply("TYPE", key); } unlink(...keys: string[]) { return this.execIntegerReply("UNLINK", ...keys); } unwatch() { return this.execStatusReply("UNWATCH"); } wait(numreplicas: number, timeout: number) { return this.execIntegerReply("WAIT", numreplicas, timeout); } watch(...keys: string[]) { return this.execStatusReply("WATCH", ...keys); } xack(key: string, group: string, ...xids: XIdInput[]) { return this.execIntegerReply( "XACK", key, group, ...xids.map((xid) => xidstr(xid)), ); } xadd( key: string, xid: XIdAdd, fieldValues: XAddFieldValues, maxlen: XMaxlen | undefined = undefined, ) { const args: RedisValue[] = [key]; if (maxlen) { args.push("MAXLEN"); if (maxlen.approx) { args.push("~"); } args.push(maxlen.elements.toString()); } args.push(xidstr(xid)); if (fieldValues instanceof Map) { for (const [f, v] of fieldValues) { args.push(f); args.push(v); } } else { for (const [f, v] of Object.entries(fieldValues)) { args.push(f); args.push(v); } } return this.execBulkReply( "XADD", ...args, ).then((rawId) => parseXId(rawId)); } xclaim(key: string, opts: XClaimOpts, ...xids: XIdInput[]) { const args = []; if (opts.idle) { args.push("IDLE"); args.push(opts.idle); } if (opts.time) { args.push("TIME"); args.push(opts.time); } if (opts.retryCount) { args.push("RETRYCOUNT"); args.push(opts.retryCount); } if (opts.force) { args.push("FORCE"); } if (opts.justXId) { args.push("JUSTID"); } return this.execArrayReply( "XCLAIM", key, opts.group, opts.consumer, opts.minIdleTime, ...xids.map((xid) => xidstr(xid)), ...args, ).then((raw) => { if (opts.justXId) { const xids = []; for (const r of raw) { if (typeof r === "string") { xids.push(parseXId(r)); } } const payload: XClaimJustXId = { kind: "justxid", xids }; return payload; } const messages = []; for (const r of raw) { if (typeof r !== "string") { messages.push(parseXMessage(r)); } } const payload: XClaimMessages = { kind: "messages", messages }; return payload; }); } xdel(key: string, ...xids: XIdInput[]) { return this.execIntegerReply( "XDEL", key, ...xids.map((rawId) => xidstr(rawId)), ); } xlen(key: string) { return this.execIntegerReply("XLEN", key); } xgroupCreate( key: string, groupName: string, xid: XIdInput | "$", mkstream?: boolean, ) { const args = []; if (mkstream) { args.push("MKSTREAM"); } return this.execStatusReply( "XGROUP", "CREATE", key, groupName, xidstr(xid), ...args, ); } xgroupDelConsumer( key: string, groupName: string, consumerName: string, ) { return this.execIntegerReply( "XGROUP", "DELCONSUMER", key, groupName, consumerName, ); } xgroupDestroy(key: string, groupName: string) { return this.execIntegerReply("XGROUP", "DESTROY", key, groupName); } xgroupHelp() { return this.execBulkReply("XGROUP", "HELP"); } xgroupSetID( key: string, groupName: string, xid: XId, ) { return this.execStatusReply( "XGROUP", "SETID", key, groupName, xidstr(xid), ); } xinfoStream(key: string) { return this.execArrayReply("XINFO", "STREAM", key).then( (raw) => { // Note that you should not rely on the fields // exact position, nor on the number of fields, // new fields may be added in the future. const data: Map = convertMap(raw); const firstEntry = parseXMessage( data.get("first-entry") as XReadIdData, ); const lastEntry = parseXMessage( data.get("last-entry") as XReadIdData, ); return { length: rawnum(data.get("length") ?? null), radixTreeKeys: rawnum(data.get("radix-tree-keys") ?? null), radixTreeNodes: rawnum(data.get("radix-tree-nodes") ?? null), groups: rawnum(data.get("groups") ?? null), lastGeneratedId: parseXId( rawstr(data.get("last-generated-id") ?? null), ), firstEntry, lastEntry, }; }, ); } xinfoStreamFull(key: string, count?: number) { const args = []; if (count !== undefined) { args.push("COUNT"); args.push(count); } return this.execArrayReply("XINFO", "STREAM", key, "FULL", ...args) .then( (raw) => { // Note that you should not rely on the fields // exact position, nor on the number of fields, // new fields may be added in the future. if (raw == null) throw "no data"; const data: Map = convertMap(raw); if (data === undefined) throw "no data converted"; const entries = (data.get("entries") as ConditionalArray).map(( raw: Raw, ) => parseXMessage(raw as XReadIdData)); return { length: rawnum(data.get("length") ?? null), radixTreeKeys: rawnum(data.get("radix-tree-keys") ?? null), radixTreeNodes: rawnum(data.get("radix-tree-nodes") ?? null), lastGeneratedId: parseXId( rawstr(data.get("last-generated-id") ?? null), ), entries, groups: parseXGroupDetail(data.get("groups") as ConditionalArray), }; }, ); } xinfoGroups(key: string) { return this.execArrayReply("XINFO", "GROUPS", key).then( (raws) => raws.map((raw) => { const data = convertMap(raw); return { name: rawstr(data.get("name") ?? null), consumers: rawnum(data.get("consumers") ?? null), pending: rawnum(data.get("pending") ?? null), lastDeliveredId: parseXId( rawstr(data.get("last-delivered-id") ?? null), ), }; }), ); } xinfoConsumers(key: string, group: string) { return this.execArrayReply( "XINFO", "CONSUMERS", key, group, ).then( (raws) => raws.map((raw) => { const data = convertMap(raw); return { name: rawstr(data.get("name") ?? null), pending: rawnum(data.get("pending") ?? null), idle: rawnum(data.get("idle") ?? null), }; }), ); } xpending( key: string, group: string, ) { return this.execArrayReply("XPENDING", key, group) .then((raw) => { if ( isNumber(raw[0]) && isString(raw[1]) && isString(raw[2]) && isCondArray(raw[3]) ) { return { count: raw[0], startId: parseXId(raw[1]), endId: parseXId(raw[2]), consumers: parseXPendingConsumers(raw[3]), }; } else { throw "parse err"; } }); } xpendingCount( key: string, group: string, startEndCount: StartEndCount, consumer?: string, ) { const args = []; args.push(xidstr(startEndCount.start)); args.push(xidstr(startEndCount.end)); args.push(startEndCount.count); if (consumer) { args.push(consumer); } return this.execArrayReply("XPENDING", key, group, ...args) .then((raw) => parseXPendingCounts(raw)); } xrange( key: string, start: XIdNeg, end: XIdPos, count?: number, ) { const args: (string | number)[] = [key, xidstr(start), xidstr(end)]; if (count !== undefined) { args.push("COUNT"); args.push(count); } return this.execArrayReply("XRANGE", ...args).then( (raw) => raw.map((m) => parseXMessage(m)), ); } xrevrange( key: string, start: XIdPos, end: XIdNeg, count?: number, ) { const args: (string | number)[] = [key, xidstr(start), xidstr(end)]; if (count !== undefined) { args.push("COUNT"); args.push(count); } return this.execArrayReply("XREVRANGE", ...args).then( (raw) => raw.map((m) => parseXMessage(m)), ); } xread( keyXIds: (XKeyId | XKeyIdLike)[], opts?: XReadOpts, ) { const args = []; if (opts) { if (opts.count !== undefined) { args.push("COUNT"); args.push(opts.count); } if (opts.block !== undefined) { args.push("BLOCK"); args.push(opts.block); } } args.push("STREAMS"); const theKeys = []; const theXIds = []; for (const a of keyXIds) { if (a instanceof Array) { // XKeyIdLike theKeys.push(a[0]); theXIds.push(xidstr(a[1])); } else { // XKeyId theKeys.push(a.key); theXIds.push(xidstr(a.xid)); } } return this.execArrayReply( "XREAD", ...args.concat(theKeys).concat(theXIds), ).then((raw) => parseXReadReply(raw)); } xreadgroup( keyXIds: (XKeyIdGroup | XKeyIdGroupLike)[], { group, consumer, count, block }: XReadGroupOpts, ) { const args: (string | number)[] = [ "GROUP", group, consumer, ]; if (count !== undefined) { args.push("COUNT"); args.push(count); } if (block !== undefined) { args.push("BLOCK"); args.push(block); } args.push("STREAMS"); const theKeys = []; const theXIds = []; for (const a of keyXIds) { if (a instanceof Array) { // XKeyIdGroupLike theKeys.push(a[0]); theXIds.push(a[1] === ">" ? ">" : xidstr(a[1])); } else { // XKeyIdGroup theKeys.push(a.key); theXIds.push(a.xid === ">" ? ">" : xidstr(a.xid)); } } return this.execArrayReply( "XREADGROUP", ...args.concat(theKeys).concat(theXIds), ).then((raw) => parseXReadReply(raw)); } xtrim(key: string, maxlen: XMaxlen) { const args = []; if (maxlen.approx) { args.push("~"); } args.push(maxlen.elements); return this.execIntegerReply("XTRIM", key, "MAXLEN", ...args); } zadd( key: string, score: number, member: string, opts?: TZAddOpts, ): Promise>; zadd( key: string, scoreMembers: [number, string][], opts?: TZAddOpts, ): Promise>; zadd( key: string, memberScores: Record, opts?: TZAddOpts, ): Promise>; zadd( key: string, param1: number | [number, string][] | Record, param2?: string | ZAddOpts, opts?: ZAddOpts, ) { const args: (string | number)[] = [key]; let isAbleToReturnNil = false; if (Array.isArray(param1)) { isAbleToReturnNil = this.pushZAddOpts(args, param2 as ZAddOpts); args.push(...param1.flatMap((e) => e)); opts = param2 as ZAddOpts; } else if (typeof param1 === "object") { isAbleToReturnNil = this.pushZAddOpts(args, param2 as ZAddOpts); for (const [member, score] of Object.entries(param1)) { args.push(score as number, member); } } else { isAbleToReturnNil = this.pushZAddOpts(args, opts); args.push(param1, param2 as string); } return isAbleToReturnNil ? this.execIntegerOrNilReply("ZADD", ...args) : this.execIntegerReply("ZADD", ...args); } private pushZAddOpts( args: (string | number)[], opts?: ZAddOpts, ): boolean { let isAbleToReturnNil = false; if (opts?.nx) { args.push("NX"); isAbleToReturnNil = true; } else if (opts?.xx) { args.push("XX"); isAbleToReturnNil = true; } else if (opts?.mode) { args.push(opts.mode); isAbleToReturnNil = true; } if (opts?.ch) { args.push("CH"); } return isAbleToReturnNil; } zaddIncr( key: string, score: number, member: string, opts?: ZAddOpts, ) { const args: (string | number)[] = [key]; this.pushZAddOpts(args, opts); args.push("INCR", score, member); return this.execBulkReply("ZADD", ...args); } zcard(key: string) { return this.execIntegerReply("ZCARD", key); } zcount(key: string, min: number, max: number) { return this.execIntegerReply("ZCOUNT", key, min, max); } zincrby(key: string, increment: number, member: string) { return this.execBulkReply("ZINCRBY", key, increment, member); } zinter( keys: string[] | [string, number][] | Record, opts?: ZInterOpts, ) { const args = this.pushZStoreArgs([], keys, opts); if (opts?.withScore) { args.push("WITHSCORES"); } return this.execArrayReply("ZINTER", ...args); } zinterstore( destination: string, keys: string[] | [string, number][] | Record, opts?: ZInterstoreOpts, ) { const args = this.pushZStoreArgs([destination], keys, opts); return this.execIntegerReply("ZINTERSTORE", ...args); } zunionstore( destination: string, keys: string[] | [string, number][] | Record, opts?: ZUnionstoreOpts, ) { const args = this.pushZStoreArgs([destination], keys, opts); return this.execIntegerReply("ZUNIONSTORE", ...args); } private pushZStoreArgs( args: (number | string)[], keys: string[] | [string, number][] | Record, opts?: ZInterstoreOpts | ZUnionstoreOpts, ) { if (Array.isArray(keys)) { args.push(keys.length); if (Array.isArray(keys[0])) { keys = keys as [string, number][]; args.push(...keys.map((e) => e[0])); args.push("WEIGHTS"); args.push(...keys.map((e) => e[1])); } else { args.push(...(keys as string[])); } } else { args.push(Object.keys(keys).length); args.push(...Object.keys(keys)); args.push("WEIGHTS"); args.push(...Object.values(keys)); } if (opts?.aggregate) { args.push("AGGREGATE", opts.aggregate); } return args; } zlexcount(key: string, min: string, max: string) { return this.execIntegerReply("ZLEXCOUNT", key, min, max); } zpopmax(key: string, count?: number) { if (count !== undefined) { return this.execArrayReply("ZPOPMAX", key, count); } return this.execArrayReply("ZPOPMAX", key); } zpopmin(key: string, count?: number) { if (count !== undefined) { return this.execArrayReply("ZPOPMIN", key, count); } return this.execArrayReply("ZPOPMIN", key); } zrange( key: string, start: number, stop: number, opts?: ZRangeOpts, ) { const args = this.pushZRangeOpts([key, start, stop], opts); return this.execArrayReply("ZRANGE", ...args); } zrangebylex( key: string, min: string, max: string, opts?: ZRangeByLexOpts, ) { const args = this.pushZRangeOpts([key, min, max], opts); return this.execArrayReply("ZRANGEBYLEX", ...args); } zrangebyscore( key: string, min: number | string, max: number | string, opts?: ZRangeByScoreOpts, ) { const args = this.pushZRangeOpts([key, min, max], opts); return this.execArrayReply("ZRANGEBYSCORE", ...args); } zrank(key: string, member: string) { return this.execIntegerOrNilReply("ZRANK", key, member); } zrem(key: string, ...members: string[]) { return this.execIntegerReply("ZREM", key, ...members); } zremrangebylex(key: string, min: string, max: string) { return this.execIntegerReply("ZREMRANGEBYLEX", key, min, max); } zremrangebyrank(key: string, start: number, stop: number) { return this.execIntegerReply("ZREMRANGEBYRANK", key, start, stop); } zremrangebyscore(key: string, min: number | string, max: number | string) { return this.execIntegerReply("ZREMRANGEBYSCORE", key, min, max); } zrevrange( key: string, start: number, stop: number, opts?: ZRangeOpts, ) { const args = this.pushZRangeOpts([key, start, stop], opts); return this.execArrayReply("ZREVRANGE", ...args); } zrevrangebylex( key: string, max: string, min: string, opts?: ZRangeByLexOpts, ) { const args = this.pushZRangeOpts([key, min, max], opts); return this.execArrayReply("ZREVRANGEBYLEX", ...args); } zrevrangebyscore( key: string, max: number, min: number, opts?: ZRangeByScoreOpts, ) { const args = this.pushZRangeOpts([key, max, min], opts); return this.execArrayReply("ZREVRANGEBYSCORE", ...args); } private pushZRangeOpts( args: (number | string)[], opts?: ZRangeOpts | ZRangeByLexOpts | ZRangeByScoreOpts, ) { if ((opts as ZRangeByScoreOpts)?.withScore) { args.push("WITHSCORES"); } if ((opts as ZRangeByScoreOpts)?.limit) { args.push( "LIMIT", (opts as ZRangeByScoreOpts).limit!.offset, (opts as ZRangeByScoreOpts).limit!.count, ); } return args; } zrevrank(key: string, member: string) { return this.execIntegerOrNilReply("ZREVRANK", key, member); } zscore(key: string, member: string) { return this.execBulkReply("ZSCORE", key, member); } scan( cursor: number, opts?: ScanOpts, ) { const args = this.pushScanOpts([cursor], opts); return this.execArrayReply("SCAN", ...args) as Promise< [BulkString, BulkString[]] >; } sscan( key: string, cursor: number, opts?: SScanOpts, ) { const args = this.pushScanOpts([key, cursor], opts); return this.execArrayReply("SSCAN", ...args) as Promise< [BulkString, BulkString[]] >; } hscan( key: string, cursor: number, opts?: HScanOpts, ) { const args = this.pushScanOpts([key, cursor], opts); return this.execArrayReply("HSCAN", ...args) as Promise< [BulkString, BulkString[]] >; } zscan( key: string, cursor: number, opts?: ZScanOpts, ) { const args = this.pushScanOpts([key, cursor], opts); return this.execArrayReply("ZSCAN", ...args) as Promise< [BulkString, BulkString[]] >; } private pushScanOpts( args: (number | string)[], opts?: ScanOpts | HScanOpts | ZScanOpts | SScanOpts, ) { if (opts?.pattern !== undefined) { args.push("MATCH", opts.pattern); } if (opts?.count !== undefined) { args.push("COUNT", opts.count); } if ((opts as ScanOpts)?.type !== undefined) { args.push("TYPE", (opts as ScanOpts).type!); } return args; } latencyDoctor() { return this.execBulkReply("LATENCY", "DOCTOR"); } tx() { return createRedisPipeline(this.client.connection, true); } pipeline() { return createRedisPipeline(this.client.connection); } } export interface RedisConnectOptions extends RedisConnectionOptions { hostname: string; port?: number | string; } /** * Connect to Redis server * @param options * @example * ```ts * import { connect } from "./mod.ts"; * const conn1 = await connect({hostname: "127.0.0.1", port: 6379}); // -> TCP, 127.0.0.1:6379 * const conn2 = await connect({hostname: "redis.proxy", port: 443, tls: true}); // -> TLS, redis.proxy:443 * ``` */ export async function connect(options: RedisConnectOptions): Promise { const { hostname, port, ...connectionOptions } = options; const connection = createRedisConnection(hostname, port, connectionOptions); await connection.connect(); const client = createDefaultClient(connection); return create(client); } /** * Create a lazy Redis client that will not establish a connection until a command is actually executed. * * ```ts * import { createLazyClient } from "./mod.ts"; * * const client = createLazyClient({ hostname: "127.0.0.1", port: 6379 }); * console.assert(!client.isConnected); * await client.get("foo"); * console.assert(client.isConnected); * ``` */ export function createLazyClient(options: RedisConnectOptions): Redis { const { hostname, port, ...connectionOptions } = options; const connection = createRedisConnection(hostname, port, connectionOptions); const baseClient = createBaseLazyClient(connection); return create(baseClient); } /** * Create {@linkcode Redis} from {@linkcode Client}. * * @deprecated This is an experimental API and may possibly be removed in the future. */ export function create(client: Client): Redis { return new RedisImpl(client); } /** * Extract RedisConnectOptions from redis URL * @param url * @example * ```ts * import { parseURL } from "./mod.ts"; * * parseURL("redis://foo:bar@localhost:6379/1"); // -> {hostname: "localhost", port: "6379", tls: false, db: 1, name: foo, password: bar} * 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} * ``` */ export function parseURL(url: string): RedisConnectOptions { const { protocol, hostname, port, username, password, pathname, searchParams, } = new URL(url); const db = pathname.replace("/", "") !== "" ? pathname.replace("/", "") : searchParams.get("db") ?? undefined; return { hostname: hostname !== "" ? hostname : "localhost", port: port !== "" ? parseInt(port, 10) : 6379, tls: protocol == "rediss:" ? true : searchParams.get("ssl") === "true", db: db ? parseInt(db, 10) : undefined, name: username !== "" ? username : undefined, password: password !== "" ? password : searchParams.get("password") ?? undefined, }; } function createBaseLazyClient(connection: Connection): Client { let client: Client | null = null; async function ensureClient(): Promise { if (!client) { client = createDefaultClient(connection); if (!connection.isConnected) { await connection.connect(); } } return client; } return { get connection() { return connection; }, exec(command, ...args) { return this.sendCommand(command, args); }, async subscribe(command, ...channelsOrPatterns) { return (client ?? await ensureClient()).subscribe( command, ...channelsOrPatterns, ); }, async sendCommand(command, args, options) { const client = await ensureClient(); return client.sendCommand(command, args, options); }, close() { if (client) { return client.close(); } }, }; } ================================================ FILE: stream.ts ================================================ import type { ConditionalArray, Raw, RedisValue, } from "./protocol/shared/types.ts"; export interface XId { unixMs: number; seqNo: number; } export interface XMessage { xid: XId; fieldValues: Record; } export interface XKeyId { key: string; xid: XIdInput; } export type XKeyIdLike = [string, XIdInput]; export interface XKeyIdGroup { key: string; xid: XIdGroupRead; } export type XKeyIdGroupLike = [string, XIdGroupRead]; export type XReadStream = { key: string; messages: XMessage[] }; export type XReadReply = XReadStream[]; // basic data returned by redis export type XReadIdData = [string, string[]]; export type XReadStreamRaw = [string, XReadIdData[]]; export type XReadReplyRaw = XReadStreamRaw[]; /** Flexible input type for commands which require message * ID to be passed (represented in lower-level Redis API as * "1000-0" etc). * * We also include an array format for ease of use, where * the first element is the epochMillis, second is seqNo. * * We also allow passing a single number, * which will represent the the epoch Millis with * seqNo of zero. (Especially useful is to pass 0.) */ export type XIdInput = XId | [number, number] | number | string; /** * ID input type for XADD, which is allowed to include the * "*" operator. */ export type XIdAdd = XIdInput | "*"; /** * ID input type for XGROUPREAD, which is allowed to include * the ">" operator. We include an array format for ease of * use, where the first element is the epochMillis, second * is seqNo. */ export type XIdGroupRead = XIdInput | ">"; /** Allows special maximum ID for XRANGE and XREVRANGE */ export type XIdPos = XIdInput | "+"; /** Allows special minimum ID for XRANGE and XREVRANGE */ export type XIdNeg = XIdInput | "-"; /** Allow special $ ID for XGROUP CREATE */ export type XIdCreateGroup = XIdInput | "$"; export type XAddFieldValues = | Record | Map; export interface XReadOpts { count?: number; block?: number; } export interface XReadGroupOpts { group: string; consumer: string; count?: number; block?: number; } export interface XMaxlen { approx?: boolean; elements: number; } export type XClaimReply = XClaimMessages | XClaimJustXId; export interface XClaimMessages { kind: "messages"; messages: XMessage[]; } export interface XClaimJustXId { kind: "justxid"; xids: XId[]; } /** * @param count Limit on the number of messages to return per call. * @param startId ID for the first pending record. * @param endId ID for the final pending record. * @param consumers Every consumer in the consumer group * with at least one pending message, and the number of * pending messages it has. */ export interface XPendingReply { count: number; startId: XId; endId: XId; consumers: XPendingConsumer[]; } export interface XPendingConsumer { name: string; pending: number; } /** * Represents a pending message parsed from xpending. * * @param id The ID of the message * @param consumer The name of the consumer that fetched the message * and has still to acknowledge it. We call it the * current owner of the message. * @param lastDeliveredMs The number of milliseconds that elapsed since the * last time this message was delivered to this consumer. * @param timesDelivered The number of times this message was delivered. */ export interface XPendingCount { xid: XId; owner: string; lastDeliveredMs: number; timesDelivered: number; } /** Used in the XPENDING command, all three of these * args must be specified if _any_ are specified. */ export interface StartEndCount { start: XIdNeg; end: XIdPos; count: number; } export interface XInfoStreamReply { length: number; radixTreeKeys: number; radixTreeNodes: number; groups: number; lastGeneratedId: XId; firstEntry: XMessage; lastEntry: XMessage; } export interface XInfoStreamFullReply { length: number; radixTreeKeys: number; radixTreeNodes: number; lastGeneratedId: XId; entries: XMessage[]; groups: XGroupDetail[]; } /** * Child of the return type for xinfo_stream_full */ export interface XGroupDetail { name: string; lastDeliveredId: XId; pelCount: number; pending: XPendingCount[]; consumers: XConsumerDetail[]; } /** Child of XINFO STREAMS FULL response */ export interface XConsumerDetail { name: string; seenTime: number; pelCount: number; pending: { xid: XId; lastDeliveredMs: number; timesDelivered: number }[]; } export type XInfoConsumersReply = XInfoConsumer[]; /** * A consumer parsed from xinfo command. * * @param name Name of the consumer group. * @param pending Number of pending messages for this specific consumer. * @param idle This consumer's idle time in milliseconds. */ export interface XInfoConsumer { name: string; pending: number; idle: number; } /** Response to XINFO GROUPS */ export type XInfoGroupsReply = XInfoGroup[]; export interface XInfoGroup { name: string; consumers: number; pending: number; lastDeliveredId: XId; } export interface XClaimOpts { group: string; consumer: string; minIdleTime: number; idle?: number; time?: number; retryCount?: number; force?: boolean; justXId?: boolean; } export function parseXMessage(raw: XReadIdData): XMessage { const fieldValues: Record = {}; let f: string | undefined = undefined; let m = 0; for (const data of raw[1]) { if (m % 2 === 0) { f = data; } else if (f) { fieldValues[f] = data; } m++; } return { xid: parseXId(raw[0]), fieldValues: fieldValues }; } export function convertMap(raw: ConditionalArray): Map { const fieldValues: Map = new Map(); let f: string | undefined = undefined; let m = 0; for (const data of raw) { if (m % 2 === 0 && typeof data === "string") { f = data; } else if (m % 2 === 1 && f) { fieldValues.set(f, data); } m++; } return fieldValues; } export function parseXReadReply(raw: XReadReplyRaw): XReadReply { const out: XReadStream[] = []; for (const [key, idData] of raw ?? []) { const messages = []; for (const rawMsg of idData) { messages.push(parseXMessage(rawMsg)); } out.push({ key, messages }); } return out; } export function parseXId(raw: string): XId { const [ms, sn] = raw.split("-"); return { unixMs: parseInt(ms), seqNo: parseInt(sn) }; } export function parseXPendingConsumers( raws: ConditionalArray, ): XPendingConsumer[] { const out: XPendingConsumer[] = []; for (const raw of raws) { if (isCondArray(raw) && isString(raw[0]) && isString(raw[1])) { out.push({ name: raw[0], pending: parseInt(raw[1]) }); } } return out; } export function parseXPendingCounts(raw: ConditionalArray): XPendingCount[] { const infos: XPendingCount[] = []; for (const r of raw) { if ( isCondArray(r) && isString(r[0]) && isString(r[1]) && isNumber(r[2]) && isNumber(r[3]) ) { infos.push({ xid: parseXId(r[0]), owner: r[1], lastDeliveredMs: r[2], timesDelivered: r[3], }); } } return infos; } export function parseXGroupDetail(rawGroups: ConditionalArray): XGroupDetail[] { const out = []; for (const rawGroup of rawGroups) { if (isCondArray(rawGroup)) { const data = convertMap(rawGroup); // array of arrays const consDeets = data.get("consumers") as ConditionalArray[]; out.push({ name: rawstr(data.get("name") ?? null), lastDeliveredId: parseXId( rawstr(data.get("last-delivered-id") ?? null), ), pelCount: rawnum(data.get("pel-count") ?? null), pending: parseXPendingCounts(data.get("pending") as ConditionalArray), consumers: parseXConsumerDetail(consDeets), }); } } return out; } export function parseXConsumerDetail(nestedRaws: Raw[][]): XConsumerDetail[] { const out: XConsumerDetail[] = []; for (const raws of nestedRaws) { const data = convertMap(raws); const pending = (data.get("pending") as [string, number, number][]).map( (p) => { return { xid: parseXId(rawstr(p[0])), lastDeliveredMs: rawnum(p[1]), timesDelivered: rawnum(p[2]), }; }, ); const r = { name: rawstr(data.get("name") ?? null), seenTime: rawnum(data.get("seen-time") ?? null), pelCount: rawnum(data.get("pel-count") ?? null), pending, }; out.push(r); } return out; } export function xidstr( xid: XIdAdd | XIdNeg | XIdPos | XIdCreateGroup | XIdGroupRead, ) { if (typeof xid === "string") return xid; if (typeof xid === "number") return `${xid}-0`; if (xid instanceof Array && xid.length > 1) return `${xid[0]}-${xid[1]}`; if (isXId(xid)) return `${xid.unixMs}-${xid.seqNo}`; throw "fail"; } export function rawnum(raw: Raw): number { return raw ? +raw.toString() : 0; } export function rawstr(raw: Raw): string { return raw ? raw.toString() : ""; } // deno-lint-ignore no-explicit-any -- intended to be used as a type guard export function isString(x: any): x is string { return typeof x === "string"; } // deno-lint-ignore no-explicit-any -- intended to be used as a type guard. export function isNumber(x: any): x is number { return typeof x === "number"; } export function isCondArray(x: Raw): x is ConditionalArray { const l = (x as ConditionalArray).length; if (l > 0 || l < 1) return true; else return false; } function isXId(xid: XIdAdd): xid is XId { return (xid as XId).unixMs !== undefined; } ================================================ FILE: subscription.ts ================================================ import type { Binary } from "./protocol/shared/types.ts"; export type DefaultPubSubMessageType = string; export type PubSubMessageType = string | string[]; export type SubscribeCommand = "SUBSCRIBE" | "PSUBSCRIBE"; export interface RedisPubSubMessage { pattern?: string; channel: string; message: TMessage; } export interface RedisSubscription< TMessage extends PubSubMessageType = DefaultPubSubMessageType, > { readonly isClosed: boolean; receive(): AsyncIterableIterator>; receiveBuffers(): AsyncIterableIterator>; psubscribe(...patterns: string[]): Promise; subscribe(...channels: string[]): Promise; punsubscribe(...patterns: string[]): Promise; unsubscribe(...channels: string[]): Promise; close(): void; } ================================================ FILE: tests/backoff_test.ts ================================================ import { assertEquals } from "../deps/std/assert.ts"; import { describe, it } from "../deps/std/testing.ts"; import { exponentialBackoff } from "../backoff.ts"; describe("backoff", { permissions: "none", }, () => { describe("exponentialBackoff", () => { it("should return exponentially increasing backoff intervals", () => { const backoff = exponentialBackoff({ multiplier: 2, maxInterval: 5000, minInterval: 1000, }); assertEquals(backoff(1), 1000); assertEquals(backoff(2), 2000); assertEquals(backoff(3), 4000); assertEquals(backoff(4), 5000); assertEquals(backoff(5), 5000); }); }); }); ================================================ FILE: tests/client_test.ts ================================================ import type { Redis } from "../redis.ts"; import { delay } from "../deps/std/async.ts"; import { assert, assertEquals, assertRejects, assertThrows, } from "../deps/std/assert.ts"; import { afterAll, afterEach, beforeAll, beforeEach, describe, it, } from "../deps/std/testing.ts"; import { newClient, nextPort, startRedis, stopRedis } from "./test_util.ts"; import type { TestServer } from "./test_util.ts"; describe("client", () => { let port!: number; let server!: TestServer; let client!: Redis; beforeAll(async () => { port = nextPort(); server = await startRedis({ port }); }); afterAll(() => stopRedis(server)); beforeEach(async () => { client = await newClient({ hostname: "127.0.0.1", port }); }); afterEach(() => client.close()); it("client caching with opt in", async () => { await client.clientTracking({ mode: "ON", optIn: true }); assertEquals(await client.clientCaching("YES"), "OK"); }); it("client caching with opt out", async () => { await client.clientTracking({ mode: "ON", optOut: true }); assertEquals(await client.clientCaching("NO"), "OK"); }); it("client caching without opt in or opt out", async () => { await assertRejects( () => { return client.clientCaching("YES"); }, Error, "-ERR CLIENT CACHING can be called only when the client is in tracking mode with OPTIN or OPTOUT mode enabled", ); }); it("client id", async () => { const id = await client.clientID(); assertEquals(typeof id, "number"); }); it("client info", async () => { const id = await client.clientID(); const info = await client.clientInfo(); assert(info!.includes(`id=${id}`)); }); it("client setname & getname", async () => { assertEquals(await client.clientSetName("deno-redis"), "OK"); assertEquals(await client.clientGetName(), "deno-redis"); }); it("client getredir with no redirect", async () => { assertEquals(await client.clientGetRedir(), -1); }); it("client getredir with redirect", async () => { const tempClient = await newClient({ hostname: "127.0.0.1", port }); try { const id = await tempClient.clientID(); await client.clientTracking({ mode: "ON", redirect: id }); assertEquals(await client.clientGetRedir(), id); } finally { tempClient.close(); } }); it("client pause & unpause", async () => { assertEquals(await client.clientPause(5), "OK"); assertEquals(await client.clientPause(5, "ALL"), "OK"); assertEquals(await client.clientPause(5, "WRITE"), "OK"); assertEquals(await client.clientUnpause(), "OK"); }); it("client kill by addr", async () => { const tempClient = await newClient({ hostname: "127.0.0.1", port }); try { const info = await client.clientInfo() as string; const addr = info.split(" ").find((s) => s.startsWith("addr=") )!.split("=")[1]; assertEquals(await tempClient.clientKill({ addr }), 1); } finally { tempClient.close(); } }); it("client kill by id", async () => { const tempClient = await newClient({ hostname: "127.0.0.1", port }); try { const id = await client.clientID(); assertEquals(await tempClient.clientKill({ id }), 1); } finally { tempClient.close(); } }); it("client list", async () => { const id = await client.clientID(); let list = await client.clientList(); assert(list!.includes(`id=${id}`)); list = await client.clientList({ type: "PUBSUB" }); assertEquals(list, ""); list = await client.clientList({ type: "NORMAL" }); assert(list!.includes(`id=${id}`)); list = await client.clientList({ ids: [id] }); assert(list!.includes(`id=${id}`)); assertThrows( () => { return client.clientList({ type: "MASTER", ids: [id] }); }, Error, "only one of `type` or `ids` can be specified", ); }); it("client tracking", async () => { assertEquals( await client.clientTracking({ mode: "ON", prefixes: ["foo", "bar"], bcast: true, }), "OK", ); assertEquals( await client.clientTracking({ mode: "ON", bcast: true, optIn: false, noLoop: true, }), "OK", ); await assertRejects( () => { return client.clientTracking({ mode: "ON", bcast: true, optIn: true }); }, Error, "-ERR OPTIN and OPTOUT are not compatible with BCAST", ); }); it("client trackinginfo", async () => { const info = await client.clientTrackingInfo(); assert(info.includes("flags")); assert(info.includes("redirect")); assert(info.includes("prefixes")); }); it("client unblock nothing", async () => { const id = await client.clientID(); assertEquals(await client.clientUnblock(id), 0); }); it("client unblock with timeout", async () => { const tempClient = await newClient({ hostname: "127.0.0.1", port }); try { const id = await tempClient.clientID(); const promise = tempClient.brpop(0, "key1"); // Block. await delay(5); // Give some leeway for brpop to reach redis. assertEquals(await client.clientUnblock(id, "TIMEOUT"), 1); await promise; } finally { tempClient.close(); } }); it("client unblock with error", async () => { const tempClient = await newClient({ hostname: "127.0.0.1", port }); try { const id = await tempClient.clientID(); const promise = assertRejects( () => tempClient.brpop(0, "key1"), Error, "-UNBLOCKED", ); await delay(5); // Give some leeway for brpop to reach redis. assertEquals(await client.clientUnblock(id, "ERROR"), 1); await promise; } finally { tempClient.close(); } }); it("client kill by type and don't skip ourselves", async () => { assertEquals(await client.clientKill({ type: "NORMAL", skipme: "NO" }), 1); }); }); ================================================ FILE: tests/cluster/test.ts ================================================ import { nextPorts, startRedisCluster, stopRedisCluster } from "./test_util.ts"; import type { TestCluster } from "./test_util.ts"; import { connect as connectToCluster } from "../../experimental/cluster/mod.ts"; import { assert, assertArrayIncludes, assertEquals, assertRejects, } from "../../deps/std/assert.ts"; import { sample } from "../../deps/std/random.ts"; import { afterAll, afterEach, beforeAll, describe, it, } from "../../deps/std/testing.ts"; import { calculateSlot } from "../../deps/cluster-key-slot.js"; import { ErrorReplyError } from "../../errors.ts"; import { connect, create } from "../../redis.ts"; import type { CommandExecutor } from "../../executor.ts"; import type { Connection } from "../../connection.ts"; import type { Redis } from "../../mod.ts"; describe("experimental/cluster", () => { let ports!: Array; let cluster!: TestCluster; let nodes!: Array<{ hostname: string; port: number }>; let client!: Redis; beforeAll(async () => { ports = nextPorts(6); cluster = await startRedisCluster(ports); nodes = ports.map((port) => ({ hostname: "127.0.0.1", port, })); client = await connectToCluster({ nodes }); }); afterAll(() => stopRedisCluster(cluster)); afterEach(() => client.close()); it("del multiple keys in the same hash slot", async () => { await client.set("{hoge}foo", "a"); await client.set("{hoge}bar", "b"); const r = await client.del("{hoge}foo", "{hoge}bar"); assertEquals(r, 2); }); it("del multiple keys in different hash slots", async () => { await client.set("foo", "a"); await client.set("bar", "b"); await assertRejects( async () => { await client.del("foo", "bar"); }, ErrorReplyError, "-CROSSSLOT Keys in request don't hash to the same slot", ); }); it("handle a -MOVED redirection error", async () => { let redirected = false; let manuallyRedirectedPort!: number; const portsSent = new Set(); const client = await connectToCluster({ nodes, async newRedis(opts) { const redis = await connect(opts); assert(opts.port != null); const proxyExecutor = { get connection(): Connection { throw new Error("Not supported"); }, close() { return redis.close(); }, exec(cmd, ...args) { return this.sendCommand(cmd, args); }, async sendCommand(cmd, args, options) { if (cmd === "GET" && !redirected) { // Manually cause a -MOVED redirection error const [key] = args ?? []; assert(typeof key === "string"); const slot = calculateSlot(key); manuallyRedirectedPort = sample( ports.filter((x) => x !== opts.port), )!; const error = new ErrorReplyError( `-MOVED ${slot} ${opts.hostname}:${manuallyRedirectedPort}`, ); redirected = true; throw error; } else { assert(opts.port); portsSent.add(Number(opts.port)); const reply = await redis.sendCommand(cmd, args, options); return reply; } }, } as CommandExecutor; return create(proxyExecutor); }, }); try { await client.set("foo", "bar"); const r = await client.get("foo"); assertEquals(r, "bar"); // Check if a cluster client correctly handles a -MOVED error assert(redirected); assertArrayIncludes([...portsSent], [manuallyRedirectedPort]); } finally { client.close(); } }); it("handle a -ASK redirection error", async () => { let redirected = false; let manuallyRedirectedPort!: number; const portsSent = new Set(); const commandsSent = new Set(); const client = await connectToCluster({ nodes, async newRedis(opts) { const redis = await connect(opts); assert(opts.port != null); const proxyExecutor = { get connection(): Connection { throw new Error("Not supported"); }, close() { return redis.close(); }, exec(cmd, ...args) { return this.sendCommand(cmd, args); }, async sendCommand(cmd, args, options) { commandsSent.add(cmd); if (cmd === "GET" && !redirected) { // Manually cause a -ASK redirection error const [key] = args ?? []; assert(typeof key === "string"); const slot = calculateSlot(key); manuallyRedirectedPort = sample( ports.filter((x) => x !== opts.port), )!; const error = new ErrorReplyError( `-ASK ${slot} ${opts.hostname}:${manuallyRedirectedPort}`, ); redirected = true; throw error; } else { assert(opts.port); portsSent.add(Number(opts.port)); const reply = await redis.sendCommand(cmd, args, options); return reply; } }, } as CommandExecutor; return create(proxyExecutor); }, }); try { await client.set("hoge", "piyo"); const r = await client.get("hoge"); assertEquals(r, "piyo"); // Check if a cluster client correctly handles a -ASK error assert(redirected); assertArrayIncludes([...portsSent], [manuallyRedirectedPort]); assertArrayIncludes([...commandsSent], ["ASKING"]); } finally { client.close(); } }); it("properly handle too many redirections", async () => { const client = await connectToCluster({ nodes, async newRedis(opts) { const redis = await connect(opts); assert(opts.port != null); const proxyExecutor = { get connection(): Connection { throw new Error("Not supported"); }, close() { return redis.close(); }, exec(cmd, ...args) { return this.sendCommand(cmd, args); }, async sendCommand(cmd, args, options) { if (cmd === "GET") { // Manually cause a -MOVED redirection error const [key] = args ?? []; assert(typeof key === "string"); const slot = calculateSlot(key); const randomPort = sample( ports.filter((x) => x !== opts.port), ); const error = new ErrorReplyError( `-MOVED ${slot} ${opts.hostname}:${randomPort}`, ); throw error; } else { const reply = await redis.sendCommand(cmd, args, options); return reply; } }, } as CommandExecutor; return create(proxyExecutor); }, }); try { await assertRejects( () => client.get("foo"), Error, "Too many Cluster redirections?", ); } finally { client.close(); } }); }); ================================================ FILE: tests/cluster/test_util.ts ================================================ import { ensureTerminated, nextPort, startRedis, stopRedis, } from "../test_util.ts"; import type { TestServer } from "../test_util.ts"; import { readAll, readerFromStreamReader } from "../../deps/std/io.ts"; import { delay } from "../../deps/std/async.ts"; export interface TestCluster { servers: TestServer[]; } export async function startRedisCluster(ports: number[]): Promise { const servers = await Promise.all(ports.map((port) => startRedis({ port, clusterEnabled: true, makeClusterConfigFile: true, }) )); const cluster = { servers }; const redisCLI = new Deno.Command("redis-cli", { args: [ "--cluster", "create", ...ports.map((port) => `127.0.0.1:${port}`), "--cluster-replicas", "1", "--cluster-yes", ], stderr: "piped", }).spawn(); try { // Wait for cluster setup to complete... const status = await redisCLI.status; if (!status.success) { stopRedisCluster(cluster); const errOutput = await readAll( readerFromStreamReader(redisCLI.stderr.getReader()), ); const decoder = new TextDecoder(); throw new Error(`Failed to setup cluster: ${decoder.decode(errOutput)}`); } // Ample time for cluster to finish startup await delay(5000); return cluster; } finally { ensureTerminated(redisCLI); } } export async function stopRedisCluster(cluster: TestCluster): Promise { for (const server of cluster.servers) { await stopRedis(server); } } export function nextPorts(n: number): Array { return Array(n).fill(0).map(() => nextPort()); } ================================================ FILE: tests/cluster_test.ts ================================================ import type { Redis } from "../mod.ts"; import { assert, assertEquals, assertStringIncludes, } from "../deps/std/assert.ts"; import { afterAll, beforeAll, describe, it } from "../deps/std/testing.ts"; import { newClient, nextPort, startRedis, stopRedis } from "./test_util.ts"; import type { TestServer } from "./test_util.ts"; describe("cluster", () => { let port1!: number; let port2!: number; let s1!: TestServer; let s2!: TestServer; let client!: Redis; beforeAll(async () => { port1 = nextPort(); port2 = nextPort(); s1 = await startRedis({ port: port1, clusterEnabled: true }); s2 = await startRedis({ port: port2, clusterEnabled: true }); client = await newClient({ hostname: "127.0.0.1", port: port2 }); }); afterAll(async () => { await stopRedis(s1); await stopRedis(s2); client.close(); }); it("addslots", async () => { await client.clusterFlushSlots(); assertEquals(await client.clusterAddSlots(1, 2, 3), "OK"); }); it("myid", async () => { assert(!!(await client.clusterMyID())); }); it("countfailurereports", async () => { const nodeId = await client.clusterMyID(); assertEquals(await client.clusterCountFailureReports(nodeId), 0); }); it("countkeysinslot", async () => { assertEquals(await client.clusterCountKeysInSlot(1), 0); }); it("delslots", async () => { assertEquals(await client.clusterDelSlots(1, 2, 3), "OK"); }); it("getkeysinslot", async () => { assertEquals(await client.clusterGetKeysInSlot(1, 1), []); }); it("flushslots", async () => { assertEquals(await client.clusterFlushSlots(), "OK"); }); it("info", async () => { assertStringIncludes(await client.clusterInfo(), "cluster_state"); }); it("keyslot", async () => { assertEquals(await client.clusterKeySlot("somekey"), 11058); }); it("meet", async () => { assertEquals(await client.clusterMeet("127.0.0.1", port2), "OK"); }); it("nodes", async () => { const nodeId = await client.clusterMyID(); const nodes = await client.clusterNodes(); assertStringIncludes(nodes, nodeId); }); it("replicas", async () => { const nodeId = await client.clusterMyID(); assertEquals(await client.clusterReplicas(nodeId), []); }); it("slaves", async () => { const nodeId = await client.clusterMyID(); assertEquals(await client.clusterSlaves(nodeId), []); }); it("forget", async () => { const nodeId = await client.clusterMyID(); const otherNode = (await client.clusterNodes()) .split("\n") .find((n) => !n.startsWith(nodeId)) ?.split(" ")[0]; if (otherNode) { assertEquals(await client.clusterForget(otherNode), "OK"); } }); it("saveconfig", async () => { assertEquals(await client.clusterSaveConfig(), "OK"); }); it("setslot", async () => { const nodeId = await client.clusterMyID(); assertEquals(await client.clusterSetSlot(1, "NODE", nodeId), "OK"); assertEquals(await client.clusterSetSlot(1, "MIGRATING", nodeId), "OK"); assertEquals(await client.clusterSetSlot(1, "STABLE"), "OK"); }); it("slots", async () => { assert(Array.isArray(await client.clusterSlots())); }); it("replicate", async () => { const nodeId = await client.clusterMyID(); const otherNode = (await client.clusterNodes()) .split("\n") .find((n) => !n.startsWith(nodeId)) ?.split(" ")[0]; if (otherNode) { assertEquals(await client.clusterReplicate(otherNode), "OK"); } }); it("failover", async () => { const nodeId = await client.clusterMyID(); const otherNode = (await client.clusterNodes()) .split("\n") .find((n) => !n.startsWith(nodeId)) ?.split(" ")[0]; if (otherNode) { assertEquals(await client.clusterFailover(), "OK"); } }); it("reset", async () => { assertEquals(await client.clusterReset(), "OK"); }); }); ================================================ FILE: tests/commands/acl.ts ================================================ import { assertArrayIncludes, assertEquals, assertStringIncludes, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, describe, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import { usesRedisVersion } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function aclTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); describe("whoami", () => { it("returns the username of the current connection", async () => { assertEquals(await client.aclWhoami(), "default"); }); }); describe("list", () => { it("returns the ACL rules", async () => { const rules = await client.aclList(); assertStringIncludes(rules[0], "user default on nopass"); assertEquals(rules.length, 1); }); }); describe("getuser", () => { it("returns the user's ACL flags", async () => { const flags = await client.aclGetUser("default"); assertArrayIncludes(flags, [ "flags", "passwords", "commands", "channels", ]); }); }); describe("cat", () => { it("returns the available ACL categories if no arguments are given", async () => { assertArrayIncludes( (await client.aclCat()).sort(), [ "keyspace", "read", "write", "set", "sortedset", "list", "hash", "string", "bitmap", "hyperloglog", "geo", "stream", "pubsub", "admin", "fast", "slow", "blocking", "dangerous", "connection", "transaction", "scripting", ].sort(), ); }); it("returns the commands in the specified category", async () => { assertArrayIncludes( (await client.aclCat("dangerous")).sort(), [ "lastsave", "shutdown", "monitor", "role", "replconf", "pfselftest", "save", "replicaof", "restore-asking", "restore", "swapdb", "slaveof", "bgsave", "debug", "bgrewriteaof", "sync", "flushdb", "keys", "psync", "pfdebug", "flushall", "failover", "info", "migrate", ], ); }); }); describe("users", () => { it("returns usernames", async () => { assertEquals(await client.aclUsers(), ["default"]); }); }); describe("setuser", () => { it("returns `OK` on success", async () => { assertEquals(await client.aclSetUser("alan", "+get"), "OK"); assertEquals(await client.aclDelUser("alan"), 1); }); }); describe("deluser", () => { it("returns the number of deleted users", async () => { assertEquals(await client.aclDelUser("alan"), 0); }); }); describe("genpass", () => { it("returns the generated password", async () => { const reply = await client.aclGenPass(); assertEquals(typeof reply, "string"); assertEquals(reply.length, 64); const testlen = 32; assertEquals((await client.aclGenPass(testlen)).length, testlen / 4); }); }); describe("auth", () => { it("returns `OK` on success", async () => { assertEquals(await client.auth("default", ""), "OK"); }); }); describe("log", () => { const randString = "balh"; beforeAll(async () => { try { await client.auth(randString, randString); } catch (_error) { // skip invalid username-password pair error } }); it("returns the ACL security events", async () => { assertEquals((await client.aclLog(1))[0][9], randString); }); it("returns `OK` when called with `RESET`", async () => { assertEquals(await client.aclLog("RESET"), "OK"); }); }); describe("module list", () => { it( "returns an empty array by default", { ignore: usesRedisVersion("8") }, async () => { assertEquals(await client.moduleList(), []); }, ); it( "returns `vectorset` module by default", { ignore: !usesRedisVersion("8") }, async () => { const moduleList = await client.moduleList(); assertStringIncludes(JSON.stringify(moduleList[0]), "vectorset"); }, ); }); } ================================================ FILE: tests/commands/connection.ts ================================================ import { createLazyClient } from "../../mod.ts"; import { assert, assertArrayIncludes, assertEquals, assertExists, assertInstanceOf, assertRejects, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, describe, it } from "../../deps/std/testing.ts"; import { delay } from "../../deps/std/async.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function connectionTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; const getOpts = () => ({ hostname: "127.0.0.1", port: getServer().port, }); beforeAll(async () => { client = await connect(getOpts()); }); afterAll(() => client.close()); describe("echo", () => { it("returns `message` as-is", async () => { assertEquals(await client.echo("Hello World"), "Hello World"); }); }); describe("ping", () => { it("returns `PONG` if no argument is given", async () => { assertEquals(await client.ping(), "PONG"); }); it("returns `message` as-is", async () => { assertEquals(await client.ping("Deno"), "Deno"); }); }); describe("quit", () => { it("closes the connection", async () => { const { port } = getServer(); const tempClient = await connect({ hostname: "127.0.0.1", port }); assertEquals(tempClient.isConnected, true); assertEquals(tempClient.isClosed, false); assertEquals(await tempClient.quit(), "OK"); assertEquals(tempClient.isConnected, false); assertEquals(tempClient.isClosed, true); }); }); describe("select", () => { it("returns `OK` on success", async () => { assertEquals(await client.select(1), "OK"); }); }); describe("hello", () => { it("works with no args", async () => { const reply = await client.hello(); assertArrayIncludes(reply, ["redis"]); }); it("supports AUTH", async () => { const reply = await client.hello({ protover: 2, auth: { username: "default", password: "" }, }); assertArrayIncludes(reply, ["redis"]); }); it("supports SETNAME", async () => { await client.hello({ protover: 2, clientName: "deno-redis", }); assertEquals(await client.clientGetName(), "deno-redis"); }); }); describe("swapdb", () => { it("returns `OK` on success", async () => { assertEquals(await client.swapdb(0, 1), "OK"); }); }); describe("health check", () => { it("should send a ping every `healthCheckInterval`", async () => { const opts = { ...getOpts(), healthCheckInterval: 10, }; const client = await connect(opts); const rawPreviousCommandStats = await client.info("commandstats"); await delay(25); const rawCurrentCommandStats = await client.info("commandstats"); client.close(); await delay(10); // NOTE: After closing the connection, no errors should occur const previousPingStats = parseCommandStats(rawPreviousCommandStats)["ping"]; const currentPingStats = parseCommandStats(rawCurrentCommandStats)["ping"]; assertExists(previousPingStats); assertExists(currentPingStats); const previousCallCount = previousPingStats["calls"]; const currentCallCount = currentPingStats["calls"]; const d = currentCallCount - previousCallCount; assert(d >= 2, `${d} should be greater than or equal to 2`); }); }); describe("createLazyClient", () => { it("returns the lazily connected client", async () => { const opts = getOpts(); const client = createLazyClient(opts); assert(!client.isConnected); try { await client.get("foo"); assert(client.isConnected); } finally { client.close(); } }); }); describe("connect()", () => { it("connects to the server", async () => { const client = await connect(getOpts()); assert(client.isConnected); client.close(); assert(!client.isConnected); await client.connect(); assert(client.isConnected); assertEquals(await client.ping(), "PONG"); client.close(); }); it("supports AbortSignal", async () => { const ac = new AbortController(); ac.abort(); const error = await assertRejects(async () => await connect({ ...getOpts(), signal: () => ac.signal, }), DOMException); assertEquals(error.name, "AbortError"); }); it("works with a lazy client", async () => { const client = createLazyClient(getOpts()); assert(!client.isConnected); await client.connect(); assert(client.isConnected); assertEquals(await client.ping(), "PONG"); client.close(); }); it("fires events", async () => { const client = await connect(getOpts()); let closeEventFired = false, endEventFired = false; const firedEvents: Array = []; client.addEventListener("close", () => { closeEventFired = true; firedEvents.push("close"); }); client.addEventListener("end", () => { endEventFired = true; firedEvents.push("end"); }); // @ts-expect-error unkwnon events should be denied client.addEventListener("no-such-event", () => { firedEvents.push("no-such-event"); }); client.close(); assertEquals(closeEventFired, true); assertEquals(endEventFired, true); assertEquals(firedEvents, ["close", "end"]); }); it("fires events with a lazy client", async () => { const client = createLazyClient(getOpts()); const firedEvents: Array = []; client.addEventListener("connect", (e) => { firedEvents.push("connect"); assertInstanceOf(e, CustomEvent); }); client.addEventListener("ready", (e) => { firedEvents.push("ready"); assertInstanceOf(e, CustomEvent); }, { once: true }); client.addEventListener("close", (e) => { firedEvents.push("close"); assertInstanceOf(e, CustomEvent); }); client.addEventListener("end", (e) => { firedEvents.push("end"); assertInstanceOf(e, CustomEvent); }); await client.exists("foo"); assertEquals(firedEvents, ["connect", "ready"]); client.close(); assertEquals(firedEvents, ["connect", "ready", "close", "end"]); await client.connect(); await client.exists("foo"); client.close(); assertEquals(firedEvents, [ "connect", "ready", "close", "end", "connect", "close", "end", ]); }); }); describe("using", () => { it("implements `Symbol.dispose`", async () => { using client = await connect(getOpts()); assert(client.isConnected); assert(!client.isClosed); }); }); } function parseCommandStats( stats: string, ): Record> { return stats.split("\r\n").reduce((statsByCommand, line) => { if (line.startsWith("#") || line.length === 0) { return statsByCommand; } const [section, details] = line.split(":"); assertExists(section); assertExists(details); const sectionPrefix = "cmdstat_"; assert(section.startsWith(sectionPrefix)); const command = section.slice(sectionPrefix.length); statsByCommand[command] = details.split(",").reduce((stats, attr) => { const [key, value] = attr.split("="); assertExists(key); assertExists(value); stats[key] = parseInt(value); return stats; }, {} as Record); return statsByCommand; }, {} as Record>); } ================================================ FILE: tests/commands/general.ts ================================================ import { ErrorReplyError } from "../../mod.ts"; import type { Redis } from "../../mod.ts"; import { assertEquals, assertRejects } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, describe, it, } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; export function generalTests( connect: Connector, getServer: () => TestServer, ): void { const getOpts = () => ({ hostname: "127.0.0.1", port: getServer().port, }); let client!: Redis; beforeAll(async () => { client = await connect(getOpts()); }); beforeEach(async () => { await client.flushdb(); }); afterAll(() => client.close()); it("can send multiple commands conccurently", async () => { let promises: Promise[] = []; for (const key of ["a", "b", "c"]) { promises.push(client.set(key, key)); } await Promise.all(promises); promises = []; for (const key of ["a", "b", "c"]) { promises.push(client.get(key)); } const [a, b, c] = await Promise.all(promises); assertEquals(a, "a"); assertEquals(b, "b"); assertEquals(c, "c"); }); it("can treat `null` and `undefined`", async () => { // @ts-expect-error This error is intended await client.set("null", null); // @ts-expect-error This error is intended await client.set("undefined", undefined); assertEquals(await client.get("null"), ""); assertEquals(await client.get("undefined"), ""); }); describe("connect", () => { it("selects the DB specified by `opts.db`", async () => { const opts = getOpts(); const key = "exists"; const client1 = await connect({ ...opts, db: 0 }); try { await client1.set(key, "aaa"); const exists = await client1.exists(key); assertEquals(exists, 1); } finally { client1.close(); } const client2 = await connect({ ...opts, db: 0 }); try { const exists = await client2.exists(key); assertEquals(exists, 1); } finally { client2.close(); } const client3 = await connect({ ...opts, db: 1 }); try { const exists = await client3.exists(key); assertEquals(exists, 0); } finally { client3.close(); } }); it("throws an error if a wrong password is given", async () => { const { port } = getOpts(); await assertRejects(async () => { await connect({ hostname: "127.0.0.1", port, password: "wrong_password", }); }, ErrorReplyError); }); it("throws an error if an empty password is given", async () => { const { port } = getOpts(); // In Redis, authentication with an empty password will always fail. await assertRejects(async () => { await connect({ hostname: "127.0.0.1", port, password: "", }); }, ErrorReplyError); }); }); describe("exists", () => { it("returns if `key` exists", async () => { const opts = getOpts(); const key = "exists"; const client1 = await connect({ ...opts, db: 0 }); await client1.set(key, "aaa"); const exists1 = await client1.exists(key); assertEquals(exists1, 1); const client2 = await connect({ ...opts, db: 1 }); const exists2 = await client2.exists(key); assertEquals(exists2, 0); client1.close(); client2.close(); }); it("can handle many keys", async () => { const reply = await client.exists( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", ); assertEquals(reply, 0); }); }); describe("invalid port", () => { for (const v of [Infinity, NaN, "", "port"]) { it(`throws an error if \`${v}\` is given`, async () => { await assertRejects( async () => { await connect({ hostname: "127.0.0.1", port: v, maxRetryCount: 0, }); }, Error, "invalid", ); }); } }); describe("sendCommand", () => { it("can handle simple types", async () => { // simple string { const reply = await client.sendCommand("SET", ["key", "a"]); assertEquals(reply, "OK"); } // bulk string { const reply = await client.sendCommand("GET", ["key"]); assertEquals(reply, "a"); } // integer { const reply = await client.sendCommand("EXISTS", ["key"]); assertEquals(reply, 1); } }); it("returns simple strings or blob strings as `Uint8Array` if `returnUint8Arrays` is set to `true`", async () => { const encoder = new TextEncoder(); await client.set("key", encoder.encode("hello")); const reply = await client.sendCommand("GET", ["key"], { returnUint8Arrays: true, }); assertEquals(reply, encoder.encode("hello")); }); }); describe("automatic reconnection", () => { it("reconnects when the connection is lost", async () => { const tempClient = await connect(getOpts()); try { const id = await tempClient.clientID(); await client.clientKill({ id }); const reply = await tempClient.ping(); assertEquals(reply, "PONG"); } finally { tempClient.close(); } }); it("fails when max retry count is exceeded", async () => { const tempClient = await connect({ ...getOpts(), maxRetryCount: 0, }); try { const id = await tempClient.clientID(); await client.clientKill({ id }); await assertRejects(() => tempClient.ping()); } finally { tempClient.close(); } }); it("does not reconnect when the connection is manually closed by the user", async () => { const tempClient = await connect(getOpts()); tempClient.close(); await assertRejects(() => tempClient.ping()); }); }); } ================================================ FILE: tests/commands/geo.ts ================================================ import { assertEquals } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import { usesRedisVersion } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function geoTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { client = await connect({ hostname: "127.0.0.1", port: getServer().port }); }); afterAll(() => client.close()); it("geoadd", async () => { assertEquals( await client.geoadd("Sicily", 13.361389, 38.115556, "Palermo"), 1, ); assertEquals( await client.geoadd("Sicily", 15.087269, 37.502669, "Catania"), 1, ); assertEquals( await client.geoadd("Sicily", { Palermo: [13.361389, 38.115556], Catania: [15.087269, 37.502669], }), 0, ); assertEquals( await client.geoadd( "Sicily", [13.361389, 38.115556, "Palermo"], [15.087269, 37.502669, "Catania"], ), 0, ); }); it("geohash", async () => { await client.geoadd("Sicily", { Palermo: [13.361389, 38.115556], Catania: [15.087269, 37.502669], }); const resp = await client.geohash("Sicily", "Palermo", "Catania", "Enna"); assertEquals(resp, ["sqc8b49rny0", "sqdtr74hyu0", null]); }); it("geopos", async () => { await client.geoadd("Sicily", { Palermo: [13.361389, 38.115556], Catania: [15.087269, 37.502669], }); const resp = await client.geopos("Sicily", "Palermo", "Catania", "Enna"); const usesRedis8 = usesRedisVersion("8"); assertEquals(resp, [ usesRedis8 ? ["13.361389338970184", "38.1155563954963"] : ["13.36138933897018433", "38.11555639549629859"], usesRedis8 ? ["15.087267458438873", "37.50266842333162"] : ["15.08726745843887329", "37.50266842333162032"], null, ]); }); it("geodist", async () => { await client.geoadd("Sicily", { Palermo: [13.361389, 38.115556], Catania: [15.087269, 37.502669], }); let resp = await client.geodist("Sicily", "Palermo", "Catania"); assertEquals(resp, "166274.1516"); resp = await client.geodist("Sicily", "Palermo", "Enna"); assertEquals(resp, null); }); it("georadius", async () => { await client.georadius("Test", 0, 1, 10, "km"); }); it("georadiusbymember", async () => { await client.georadiusbymember("Sicily", "Palermo", 10, "km"); }); } ================================================ FILE: tests/commands/hash.ts ================================================ import { assertEquals } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function hashTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("hdel", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hdel("key", "f1", "f2", "f3"), 2); }); it("hexists", async () => { await client.hset("key", "f1", "1"); assertEquals(await client.hexists("key", "f1"), 1); assertEquals(await client.hexists("key", "f2"), 0); }); it("hget", async () => { await client.hset("key", "f1", "1"); assertEquals(await client.hget("key", "f1"), "1"); assertEquals(await client.hget("key", "f2"), null); }); it("hgetall", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hgetall("key"), ["f1", "1", "f2", "2"]); }); it("hincrby", async () => { await client.hset("key", "f1", "1"); assertEquals(await client.hincrby("key", "f1", 4), 5); }); it("hincybyfloat", async () => { await client.hset("key", "f1", "1"); assertEquals(await client.hincrbyfloat("key", "f1", 4.33), "5.33"); }); it("hkeys", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hkeys("key"), ["f1", "f2"]); }); it("hlen", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hlen("key"), 2); }); it("hmget", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hmget("key", "f1", "f2", "f3"), [ "1", "2", null, ]); }); it("hmset", async () => { assertEquals(await client.hmset("key", "f1", "1"), "OK"); assertEquals(await client.hmset("key", { f1: "1", f2: "2" }), "OK"); assertEquals(await client.hmset("key", ["f4", "4"], ["f5", "5"]), "OK"); }); it("hset", async () => { assertEquals(await client.hset("key", "f1", "1"), 1); assertEquals(await client.hset("key", { f2: "2", f3: "3" }), 2); assertEquals(await client.hset("key", ["f4", "4"], ["f5", "5"]), 2); }); it("hsetnx", async () => { await client.hset("key", "f1", "1"); assertEquals(await client.hsetnx("key", "f1", "1"), 0); assertEquals(await client.hsetnx("key", "f2", "2"), 1); }); it("hstrlen", async () => { await client.hset("key", "f1", "abc"); assertEquals(await client.hstrlen("key", "f1"), 3); }); it("hvals", async () => { await client.hset("key", "f1", "1"); await client.hset("key", "f2", "2"); assertEquals(await client.hvals("key"), ["1", "2"]); }); it("hscan", async () => { assertEquals(Array.isArray(await client.hscan("key", 0)), true); }); } ================================================ FILE: tests/commands/hyper_loglog.ts ================================================ import { assertEquals } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function hyperloglogTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); it("pdfadd", async () => { assertEquals(await client.pfadd("hll", "a", "b", "c", "d"), 1); }); it("pdfcount", async () => { await client.pfadd("hll", "a", "b", "c", "d"); assertEquals(await client.pfcount("hll"), 4); }); it("pfmerge", async () => { await client.pfadd("hll", "a", "b", "c", "d"); await client.pfadd("hll2", "1", "2", "3", "4"); assertEquals(await client.pfmerge("hll", "hll2"), "OK"); }); } ================================================ FILE: tests/commands/key.ts ================================================ import { assert, assertArrayIncludes, assertEquals, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function keyTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const { port } = getServer(); client = await connect({ hostname: "127.0.0.1", port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("del", async () => { let s = await client.set("key1", "fuga"); assertEquals(s, "OK"); s = await client.set("key2", "fugaaa"); assertEquals(s, "OK"); const deleted = await client.del("key1", "key2"); assertEquals(deleted, 2); }); it("dump and restore", async () => { await client.set("key", "hello"); const v = await client.dump("key"); await client.del("key"); await client.restore("key", 2000, v!); assertEquals(await client.get("key"), "hello"); }); it("exists", async () => { const none = await client.exists("none", "none2"); assertEquals(none, 0); await client.set("exists", "aaa"); const exists = await client.exists("exists", "none"); assertEquals(exists, 1); }); it("expire", async () => { await client.set("key", "foo"); const v = await client.expire("key", 1); assertEquals(v, 1); }); it("expireat", async () => { await client.set("key", "bar"); const timestamp = String(new Date(8640000000000000).getTime() / 1000); const v = await client.expireat("key", timestamp); assertEquals(v, 1); }); it("keys", async () => { await client.set("key1", "foo"); await client.set("key2", "bar"); const v = await client.keys("key*"); assertEquals(v.sort(), ["key1", "key2"]); }); it("migrate", async () => { const { port } = getServer(); const v = await client.migrate("127.0.0.1", port, "nosuchkey", "0", 0); assertEquals(v, "NOKEY"); }); it("move", async () => { const v = await client.move("nosuchkey", "1"); assertEquals(v, 0); }); it("object refcount", async () => { await client.set("key", "hello"); const v = await client.objectRefCount("key"); assertEquals(v, 1); }); it("object encoding", async () => { await client.set("key", "foobar"); const v = await client.objectEncoding("key"); assertEquals(typeof v, "string"); }); it("object idletime", async () => { await client.set("key", "baz"); const v = await client.objectIdletime("key"); assertEquals(v, 0); }); it("object freq", async () => { const v = await client.objectFreq("nosuchkey"); assertEquals(v, null); }); it("object help", async () => { const v = await client.objectHelp(); assert(Array.isArray(v)); }); it("persist", async () => { const v = await client.persist("nosuckey"); assertEquals(v, 0); }); it("pexpire", async () => { await client.set("key", "hello"); const v = await client.pexpire("key", 500); assertEquals(v, 1); }); it("pexpireat", async () => { await client.set("key", "bar"); const timestamp = new Date(8640000000000000).getTime(); const v = await client.pexpireat("key", timestamp); assertEquals(v, 1); }); it("pttl", async () => { await client.set("key", "foo"); const v = await client.pttl("key"); assertEquals(v, -1); }); it("randomkey", async () => { await client.set("key", "hello"); const v = await client.randomkey(); assertEquals(typeof v, "string"); }); it("rename", async () => { await client.set("key", "foo"); const v = await client.rename("key", "newkey"); assertEquals(v, "OK"); }); it("renamenx", async () => { await client.set("key", "bar"); const v = await client.renamenx("key", "newkey"); assertEquals(v, 1); }); it("sort", async () => { await client.rpush("key", "3", "10", "5", "1"); const v = await client.sort("key"); assertEquals(v, ["1", "3", "5", "10"]); }); it("sort with multiple patterns", async () => { // https://github.com/denodrivers/redis/pull/364 await client.rpush("ids", "1", "2", "3"); await client.mset({ "weight_1": "8", "weight_2": "2", "weight_3": "5", "name_1": "foo", "name_2": "bar", "name_3": "baz", }); const result = await client.sort("ids", { by: "weight_*", patterns: ["#", "name_*"], }); assertEquals(result, [ "2", "bar", "3", "baz", "1", "foo", ]); }); it("touch", async () => { await client.set("key1", "baz"); await client.set("key2", "qux"); const v = await client.touch("key1", "key2"); assertEquals(v, 2); }); it("ttl", async () => { await client.set("key", "foo"); const v = await client.ttl("key"); assertEquals(v, -1); }); it("type", async () => { await client.set("key", "foobar"); const v = await client.type("key"); assertEquals(v, "string"); }); it("unlink", async () => { await client.set("key1", "hello"); await client.set("key2", "world"); const v = await client.unlink("key1", "key2", "nosuchkey"); assertEquals(v, 2); }); it("wait", async () => { await client.set("key", "hello"); const v = await client.wait(0, 1000); assertEquals(v, 0); }); it("scan", async () => { await client.set("key1", "foo"); await client.set("key2", "bar"); const v = await client.scan(0); assertEquals(v.length, 2); assertEquals(v[0], "0"); assertEquals(v[1].length, 2); assertArrayIncludes(v[1], ["key1", "key2"]); }); it("scan with pattern", async () => { await client.set("foo", "f"); await client.set("bar", "b"); const v = await client.scan(0, { pattern: "f*" }); assertEquals(v, ["0", ["foo"]]); }); } ================================================ FILE: tests/commands/latency.ts ================================================ import { assertStringIncludes } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, describe, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function latencyTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; const getOpts = () => ({ hostname: "127.0.0.1", port: getServer().port, }); beforeAll(async () => { client = await connect(getOpts()); }); afterAll(() => client.close()); describe("latencyDoctor", () => { it("executes `LATENCY DOCTOR`", async () => { const report = await client.latencyDoctor(); assertStringIncludes( report, "Latency monitoring is disabled in this Redis instance.", ); }); }); } ================================================ FILE: tests/commands/list.ts ================================================ import { assertEquals } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, describe, it, } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function listTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("blpop", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.blpop(2, "list"), ["list", "1"]); }); it("blpop returns null on timeout", async () => { assertEquals(await client.blpop(1, "list"), null); }); it("brpop", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.brpop(2, "list"), ["list", "2"]); }); it("brpop returns null on timeout", async () => { assertEquals(await client.brpop(1, "list"), null); }); it("brpoplpush", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.brpoplpush("list", "list", 2), "2"); }); it("brpoplpush returns null on timeout", async () => { assertEquals(await client.brpoplpush("list", "list", 1), null); }); it("lindex", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lindex("list", 0), "1"); assertEquals(await client.lindex("list", 3), null); }); it("linsert", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.linsert("list", "BEFORE", "2", "1.5"), 3); }); it("llen", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.llen("list"), 2); }); describe("lpop", () => { it("pops the first element of the list", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lpop("list"), "1"); }); it("supports the `count` argument", async () => { await client.rpush("list", "foo", "bar", "baz"); assertEquals(await client.lpop("list", 2), ["foo", "bar"]); }); }); it("lpos", async () => { await client.rpush("list", "a", "b", "c", "1"); assertEquals(await client.lpos("list", "c"), 2); assertEquals(await client.lpos("list", "d"), null); }); it("lpos with rank", async () => { await client.rpush("list", "a", "b", "c", "1", "2", "c", "c", "d"); assertEquals(await client.lpos("list", "c", { rank: 2 }), 5); }); it("lpos with count", async () => { await client.rpush("list", "a", "b", "c", "1", "2", "b", "c"); assertEquals(await client.lpos("list", "b", { count: 2 }), [1, 5]); }); it("lpos with maxlen", async () => { await client.rpush("list", "a", "b", "c"); assertEquals(await client.lpos("list", "c", { maxlen: 2 }), null); assertEquals(await client.lpos("list", "c", { maxlen: 3 }), 2); }); it("lpush", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lpush("list", "3", "4"), 4); }); it("lpushx", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lpushx("list", "3"), 3); }); it("lrange", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lrange("list", 0, -1), ["1", "2"]); }); it("lrem", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lrem("list", 0, "1"), 1); }); it("lset", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.lset("list", 0, "0"), "OK"); }); it("ltrim", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.ltrim("list", 0, 1), "OK"); }); it("rpop", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.rpop("list"), "2"); }); it("rpoplpush", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.rpoplpush("list", "list"), "2"); }); it("rpush", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.rpush("list", "3"), 3); }); it("rpoplpush", async () => { await client.rpush("list", "1", "2"); assertEquals(await client.rpushx("list", "3"), 3); }); } ================================================ FILE: tests/commands/pipeline.ts ================================================ import type { Raw } from "../../mod.ts"; import { ErrorReplyError } from "../../mod.ts"; import { assert, assertEquals } from "../../deps/std/assert.ts"; import { it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; export function pipelineTests( connect: Connector, getServer: () => TestServer, ): void { const getOpts = () => ({ hostname: "127.0.0.1", port: getServer().port, }); it("testPipeline", async () => { const opts = getOpts(); const client = await connect(opts); const pl = client.pipeline(); await Promise.all([ pl.ping(), pl.ping(), pl.set("set1", "value1"), pl.set("set2", "value2"), pl.mget("set1", "set2"), pl.del("set1"), pl.del("set2"), ]); const ret = await pl.flush(); assertEquals(ret, [ "PONG", "PONG", "OK", "OK", ["value1", "value2"], 1, 1, ]); client.close(); }); it("testTx", async () => { const opts = getOpts(); const client = await connect(opts); const tx1 = client.tx(); const tx2 = client.tx(); const tx3 = client.tx(); await client.del("key"); await Promise.all([ tx1.get("key"), tx1.incr("key"), tx1.incr("key"), tx1.incr("key"), tx1.get("key"), // tx2.get("key"), tx2.incr("key"), tx2.incr("key"), tx2.incr("key"), tx2.get("key"), // tx3.get("key"), tx3.incr("key"), tx3.incr("key"), tx3.incr("key"), tx3.get("key"), ]); const rep1 = await tx1.flush() as Array; const rep2 = await tx2.flush() as Array; const rep3 = await tx3.flush() as Array; assertEquals( parseInt(rep1[4] as string), parseInt(rep1[0] as string) + 3, ); assertEquals( parseInt(rep2[4] as string), parseInt(rep2[0] as string) + 3, ); assertEquals( parseInt(rep3[4] as string), parseInt(rep3[0] as string) + 3, ); client.close(); }); it("pipeline in concurrent", async () => { { const opts = getOpts(); const client = await connect(opts); const tx = client.pipeline(); const promises: Promise[] = []; await client.del("a", "b", "c"); for (const key of ["a", "b", "c"]) { promises.push(tx.set(key, key)); } promises.push(tx.flush()); for (const key of ["a", "b", "c"]) { promises.push(tx.get(key)); } promises.push(tx.flush()); const res = await Promise.all(promises); assertEquals(res, [ "OK", // set(a) "OK", // set(b) "OK", // set(c) ["OK", "OK", "OK"], // flush() "OK", // get(a) "OK", // get(b) "OK", // get(c) ["a", "b", "c"], // flush() ]); client.close(); } }); it("pipeline in concurrent, avoid redundant response mixup", async () => { { const opts = getOpts(); const client = await connect(opts); const randomValues = new Array(10) .fill(0) .map(() => new Array(10).fill(0).map(() => Math.random().toString())); for (let i = 0; i < 10; i++) { const key = `list_${i}`; const values = randomValues[i]; await client.del(key); await client.rpush(key, ...values); } const task = async () => { const tx = client.pipeline(); for (let i = 0; i < 10; i++) { tx.lrange(`list_${i}`, 0, -1); } return await tx.flush(); }; const res = await Promise.all([task(), task()]); assertEquals(res, [randomValues, randomValues]); client.close(); } }); it("error while pipeline", async () => { const opts = getOpts(); const client = await connect(opts); const tx = client.pipeline(); tx.set("a", "a"); tx.eval("var", ["k"], ["v"]); tx.get("a"); const resp = await tx.flush() as Array; assertEquals(resp.length, 3); assertEquals(resp[0], "OK"); assert(resp[1] instanceof ErrorReplyError); assertEquals(resp[2], "a"); client.close(); }); } ================================================ FILE: tests/commands/pubsub.ts ================================================ import { delay } from "../../deps/std/async.ts"; import { assert, assertEquals, assertRejects } from "../../deps/std/assert.ts"; import { describe, it } from "../../deps/std/testing.ts"; import { nextPort, startRedis, stopRedis } from "../test_util.ts"; import type { Connector, TestServer } from "../test_util.ts"; export function pubsubTests( connect: Connector, getServer: () => TestServer, ): void { const getOpts = () => ({ hostname: "127.0.0.1", port: getServer().port }); it("supports unsubscribing channels by `unsubscribe()`", async () => { const opts = getOpts(); const client = await connect(opts); const sub = await client.subscribe("subsc"); await sub.unsubscribe("subsc"); sub.close(); assertEquals(sub.isClosed, true); client.close(); }); it("supports reading messages sequentially by `receive()`", async () => { const opts = getOpts(); const client = await connect(opts); const pub = await connect(opts); const sub = await client.subscribe("subsc2"); const p = (async function () { const it = sub.receive(); return (await it.next()).value; })(); await pub.publish("subsc2", "wayway"); const message = await p; assertEquals(message, { channel: "subsc2", message: "wayway", }); sub.close(); assertEquals(sub.isClosed, true); assertEquals(client.isClosed, true); pub.close(); await assertRejects(async () => { await client.get("aaa"); }, Deno.errors.BadResource); }); describe("receiveBuffers", () => { it("returns messages as Uint8Array", async () => { const opts = getOpts(); const client = await connect(opts); const pub = await connect(opts); const sub = await client.subscribe("subsc3"); const p = (async () => { const it = sub.receiveBuffers(); return (await it.next()).value; })(); try { await pub.publish("subsc3", "foobar"); const message = await p; assertEquals(message, { channel: "subsc3", message: new TextEncoder().encode("foobar"), }); } finally { sub.close(); pub.close(); } assert(sub.isClosed); assert(client.isClosed); }); }); it("supports `psubscribe()`", async () => { const opts = getOpts(); const client = await connect(opts); const pub = await connect(opts); const sub = await client.psubscribe("ps*"); let message1; let message2; const it = sub.receive(); const p = (async function () { message1 = (await it.next()).value; message2 = (await it.next()).value; })(); await pub.publish("psub", "wayway"); await pub.publish("psubs", "heyhey"); await p; assertEquals(message1, { pattern: "ps*", channel: "psub", message: "wayway", }); assertEquals(message2, { pattern: "ps*", channel: "psubs", message: "heyhey", }); sub.close(); pub.close(); client.close(); }); it("supports automatic reconnection of subscribers", async () => { const opts = getOpts(); const port = nextPort(); let tempServer = await startRedis({ port }); const subscriberClient = await connect({ ...opts, port }); const backoff = () => 1200; const publisher = await connect({ ...opts, backoff, maxRetryCount: 10, port, }); const subscription = await subscriberClient.psubscribe("ps*"); const it = subscription.receive(); let messages = 0; const interval = setInterval(async () => { await publisher.publish("psub", "wayway"); messages++; }, 900); // Intentionally stops the server after the first message is delivered. setTimeout(() => stopRedis(tempServer), 1000); const { promise, resolve, reject } = Promise.withResolvers(); setTimeout(async () => { try { // At this point, the server is assumed to be stopped. // The subscriber and publisher should attempt to reconnect. assertEquals( subscriberClient.isConnected, false, "The subscriber client still thinks it is connected.", ); assertEquals( publisher.isConnected, false, "The publisher client still thinks it is connected.", ); assert(messages >= 1, "At least one message should be published."); assert(messages < 5, "Too many messages were published."); // Reboot the server. tempServer = await startRedis({ port }); const tempClient = await connect({ ...opts, port }); await tempClient.ping(); tempClient.close(); // Wait for the subscriber and publisher to reconnect... await delay(1000); assert( subscriberClient.isConnected, "The subscriber client is not connected.", ); assert(publisher.isConnected, "The publisher client is not connected."); resolve(); } catch (error) { reject(error); } }, 2000); // Block until all resolve await Promise.all([it.next(), it.next(), it.next(), it.next(), it.next()]); // Cleanup clearInterval(interval); subscription.close(); publisher.close(); subscriberClient.close(); await stopRedis(tempServer); await promise; }); it({ ignore: true, name: "SubscriptionShouldNotThrowBadResourceErrorWhenConnectionIsClosed (#89)", fn: async () => { const opts = getOpts(); const redis = await connect(opts); const sub = await redis.subscribe("test"); const subscriptionPromise = (async () => { // deno-lint-ignore no-empty -- Verifying that no exceptions are thrown. for await (const _ of sub.receive()) {} })(); redis.close(); await subscriptionPromise; assert(sub.isClosed); }, }); it("supports `pubsubNumsub()`", async () => { const opts = getOpts(); const subClient1 = await connect(opts); await subClient1.subscribe("test1", "test2"); const subClient2 = await connect(opts); await subClient2.subscribe("test2", "test3"); const pubClient = await connect(opts); const resp = await pubClient.pubsubNumsub("test1", "test2", "test3"); assertEquals(resp, ["test1", 1, "test2", 2, "test3", 1]); subClient1.close(); subClient2.close(); pubClient.close(); }); it("supports calling `subscribe()` multiple times", async () => { // https://github.com/denodrivers/redis/issues/390 const opts = getOpts(); const redis = await connect(opts); const pub = await connect(opts); const channel1 = "foo"; const channel2 = "bar"; // First subscription const sub1 = await redis.subscribe(channel1); const it1 = sub1.receive(); const promise1 = it1.next(); try { // Second subscription const sub2 = await redis.subscribe(channel2); try { const message = "A"; await pub.publish(channel1, message); const result = await promise1; assert(!result.done); assertEquals(result.value, { channel: channel1, message }); const it2 = sub2.receive(); const promise2 = it2.next(); const message2 = "B"; await pub.publish(channel2, message2); const result2 = await promise2; assert(!result2.done); assertEquals(result2.value, { channel: channel2, message: message2, }); } finally { sub2.close(); } } finally { pub.close(); sub1.close(); redis.close(); } }); } ================================================ FILE: tests/commands/resp3.ts ================================================ import { assert, assertArrayIncludes, assertEquals, assertStrictEquals, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; import { kUnstableProtover } from "../../internal/symbols.ts"; export function resp3Tests( _connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; const connect = () => _connect({ hostname: "127.0.0.1", port: getServer().port, [kUnstableProtover]: 3, }); beforeAll(async () => { client = await connect(); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("returns a double reply as a string", async () => { client.zadd("key", { one: 123, two: 2 }); assertEquals(await client.zscore("key", "one"), "123"); }); it("returns a map reply as an array", async () => { await client.hset("key", "foo", "1"); await client.hset("key", "bar", "2"); assertEquals(await client.hgetall("key"), ["foo", "1", "bar", "2"]); }); it("returns a set reply as an array", async () => { await client.sadd("key", "foo", "1"); const reply = await client.smembers("key"); assertArrayIncludes(reply, ["foo", "1"]); assertEquals(reply.length, 2); }); it("returns a null reply as `null`", async () => { const reply = await client.get("no-such-key"); assertStrictEquals(reply, null); }); it("returns a boolean reply as 0 or 1", async () => { { const reply = await client.eval("redis.setresp(3); return true", [], []); assertStrictEquals(reply, 1); } { const reply = await client.eval("redis.setresp(3); return false", [], []); assertStrictEquals(reply, 0); } }); it("supports a verbatim string", async () => { const reply = await client.latencyDoctor(); assertStrictEquals(typeof reply, "string"); assert(reply.startsWith("txt:"), `"${reply}" should start with "txt:"`); }); it("supports a push reply", async () => { using client = await connect(); const channel = "testing"; const sub = await client.subscribe(channel); const it = sub.receive(); const payload = "foobar"; await client.publish(channel, payload); const result = await it.next(); assert(!result.done); assertEquals(result.value, { channel, message: payload }); }); // deno-lint-ignore deno-lint-plugin-extra-rules/no-disabled-tests -- TODO: Support the execution of a regular command in a push-mode connection. it.skip("supports executing a regular command in a push-mode connection", () => {}); // deno-lint-ignore deno-lint-plugin-extra-rules/no-disabled-tests -- TODO: Currently, there is no command that returns a big number. it.skip("supports a big number", () => {}); // 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. it.skip("supports a blob error", () => {}); } ================================================ FILE: tests/commands/script.ts ================================================ import { assert, assertEquals } from "../../deps/std/assert.ts"; import { afterAll, afterEach, beforeAll, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function scriptTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); afterEach(async () => { await client.flushdb(); }); it("eval", async () => { const raw = await client.eval( "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["1", "2"], ["3", "4"], ); assert(Array.isArray(raw)); assertEquals(raw, ["1", "2", "3", "4"]); }); it("evalsha", async () => { const hash = await client.scriptLoad( `return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}`, ); try { assertEquals( await client.scriptExists(hash), [1], ); const result = await client.evalsha(hash, ["a", "b"], ["1", "2"]); assert(Array.isArray(result)); assertEquals(result, ["a", "b", "1", "2"]); } finally { await client.scriptFlush(); } }); } ================================================ FILE: tests/commands/set.ts ================================================ import { assert, assertArrayIncludes, assertEquals, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, beforeEach, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function setTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("sadd", async () => { assertEquals(await client.sadd("key", "1", "2", "1"), 2); }); it("scard", async () => { await client.sadd("key", "1", "2"); assertEquals(await client.scard("key"), 2); }); it("sdiff", async () => { await client.sadd("key", "1", "2"); await client.sadd("key2", "1", "3"); assertArrayIncludes(await client.sdiff("key", "key2"), ["2"]); }); it("sdiffstore", async () => { await client.sadd("key", "1", "2"); await client.sadd("key2", "1", "3"); assertEquals(await client.sdiffstore("dest", "key", "key2"), 1); }); it("sinter", async () => { await client.sadd("key", "1", "2"); await client.sadd("key2", "1", "3"); assertArrayIncludes(await client.sinter("key", "key2"), ["1"]); }); it("sinterstore", async () => { await client.sadd("key", "1", "2"); await client.sadd("key2", "1", "3"); assertEquals(await client.sinterstore("dest", "key", "key2"), 1); }); it("sismember", async () => { await client.sadd("key", "1", "2"); assertEquals(await client.sismember("key", "1"), 1); }); it("smembers", async () => { await client.sadd("key", "1", "2"); assertArrayIncludes(await client.smembers("key"), ["1", "2"]); }); it("smove", async () => { await client.sadd("key", "1", "2"); assertEquals(await client.smove("key", "dest", "1"), 1); }); it("spop", async () => { await client.sadd("key", "a"); const v = await client.spop("key"); assertEquals(v, "a"); }); it("spop with count", async () => { await client.sadd("key", "a", "b"); const v = await client.spop("key", 2); assertArrayIncludes(v, ["a", "b"]); }); it("srandmember", async () => { await client.sadd("key", "a", "b"); const v = await client.srandmember("key"); assertArrayIncludes(["a", "b"], [v]); }); it("srandmember with count", async () => { await client.sadd("key", "a", "b"); const v = await client.srandmember("key", 3); assertArrayIncludes(["a", "b", undefined], v); }); it("srem", async () => { await client.sadd("key", "a", "b"); assertEquals(await client.srem("key", "a"), 1); }); it("sunion", async () => { await client.sadd("key", "a", "b"); await client.sadd("key2", "b", "c"); const v = await client.sunion("key", "key2"); assertArrayIncludes(v, ["a", "b", "c"]); }); it("sunionstore", async () => { await client.sadd("key", "a", "b"); await client.sadd("key2", "b", "c"); const v = await client.sunionstore("dest", "key", "key2"); assertEquals(v, 3); }); it("sscan", async () => { await client.sadd("key", "a", "b"); const v = await client.sscan("key", 0); assert(Array.isArray(v)); }); } ================================================ FILE: tests/commands/sorted_set.ts ================================================ import { assert, assertEquals } from "../../deps/std/assert.ts"; import type { IsExact } from "../../deps/std/testing.ts"; import { afterAll, assertType, beforeAll, beforeEach, describe, it, } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function zsetTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("bzpopmin", async () => { await client.zadd("key", { "1": 1, "2": 2 }); assertEquals(await client.bzpopmin(1, "key"), ["key", "1", "1"]); }); it("bzpopmin returns null on timeout", async () => { const arr = await client.bzpopmin(1, "key"); assertEquals(arr, null); }); it("bzpopmax", async () => { await client.zadd("key", { "1": 1, "2": 2 }); assertEquals(await client.bzpopmax(1, "key"), ["key", "2", "2"]); }); it("bzpopmax returns null on timeout", async () => { const arr = await client.bzpopmax(1, "key"); assertEquals(arr, null); }); describe("zadd", () => { it("adds specified members to a sorted set", async () => { const v = await client.zadd("key", { "1": 1, "2": 2 }); assertEquals(v, 2); const v2 = await client.zadd("key", 3, "3"); assertEquals(v2, 1); const v3 = await client.zadd("key", [ [4, "4"], [5, "5"], ]); assertEquals( v3, 2, ); assertType>(true); }); it("supports `NX` and `XX`", async () => { const key = "zaddWithNXOrXX"; const v = await client.zadd(key, 1, "1", { nx: true }); assertEquals(v, 1); assertType>(true); const v2 = await client.zadd(key, 2, "1", { mode: "NX" }); assertEquals(v2, 0); // NOTE: In RESP3, this seems to be `null` const v3 = await client.zadd(key, 3, "1", { xx: true, ch: true }); assertEquals(v3, 1); assertType>(true); const v4 = await client.zadd(key, [[1, "2"]], { mode: "XX", }); assertEquals(v4, 0); // NOTE: In RESP3, this seems to be `null` }); it("supports `CH`", async () => { assertEquals(await client.zadd("keyWithCH", [[1, "foo"], [2, "bar"]]), 2); const v = await client.zadd( "keyWithCH", { "foo": 1, "bar": 3, "baz": 4 }, { ch: true }, ); assertEquals( v, 2, ); assertType>(true); }); }); it("zaddIncr", async () => { await client.zadd("key", 1, "a"); await client.zaddIncr("key", 2, "a"); assertEquals(await client.zscore("key", "a"), "3"); }); it("zaddIncrWithMode", async () => { assertEquals( await client.zaddIncr("key", 1, "one", { mode: "XX" }), null, "no member should be added", ); assertEquals( await client.zaddIncr("key", 2, "two", { mode: "NX" }), "2", ); }); it("zaddIncrWithCH", async () => { await client.zadd("key", 1, "foo"); assertEquals( await client.zaddIncr("key", 3, "foo", { ch: true }), "4", "`ZADD` with `INCR` should return the new score of member", ); assertEquals(await client.zscore("key", "foo"), "4"); }); it("zcount", async () => { await client.zadd("key", { "1": 1, "2": 2 }); assertEquals(await client.zcount("key", 0, 1), 1); }); it("zincrby", async () => { await client.zadd("key", { "1": 1, "2": 2 }); const v = await client.zincrby("key", 2.0, "1"); assert(v != null); assert(parseFloat(v) - 3.0 < Number.EPSILON); }); it("zinterstore", async () => { await client.zadd("key", { "1": 1, "2": 2 }); await client.zadd("key2", { "1": 1, "3": 3 }); assertEquals(await client.zinterstore("dest", ["key", "key2"]), 1); }); it("zinter", async () => { await client.zadd("key", { "1": 1, "2": 2 }); await client.zadd("key2", { "1": 1, "3": 3 }); assertEquals(await client.zinter(["key", "key2"]), ["1"]); assertEquals(await client.zinter([["key", 2], ["key2", 3]]), ["1"]); assertEquals( await client.zinter([ ["key", 2], ["key2", 3], ], { aggregate: "MIN" }), ["1"], ); assertEquals( await client.zinter(["key", "key2"], { withScore: true, }), ["1", "2"], ); }); it("zlexcount", async () => { await client.zadd("key2", { "1": 1, "2": 2 }); assertEquals(await client.zlexcount("key", "-", "(2"), 0); }); it("zpopmax", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zpopmax("key", 1), ["two", "2"]); }); it("zrange", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrange("key", 1, 2), ["two"]); }); it("zrangebylex", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrangebylex("key", "-", "(2"), []); }); it("zrevrangebylex", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrevrangebylex("key", "(2", "-"), []); }); it("zrangebyscore", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrangebyscore("key", "1", "2"), ["one", "two"]); }); it("zrank", async () => { await client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrank("key", "two"), 1); }); it("zrem", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrem("key", "one"), 1); }); it("zremrangebylex", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zremrangebylex("key", "[one", "[two"), 2); }); it("zremrangebyrank", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zremrangebyrank("key", 1, 2), 1); }); it("zremrangebyscore", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zremrangebyscore("key", 1, 2), 2); }); it("zrevrange", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrevrange("key", 1, 2), ["one"]); }); it("zrevrangebyscore", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrevrangebyscore("key", 2, 1), ["two", "one"]); }); it("zrevrank", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zrevrank("key", "one"), 1); }); it("zscore", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zscore("key", "one"), "1"); }); it("zunionstore", async () => { client.zadd("key", { one: 1, two: 2 }); client.zadd("key2", { one: 1, three: 3 }); assertEquals(await client.zunionstore("dest", ["key", "key2"]), 3); }); it("zscan", async () => { client.zadd("key", { one: 1, two: 2 }); assertEquals(await client.zscan("key", 1), ["0", ["one", "1", "two", "2"]]); }); it("testZrange", async function testZrange() { client.zadd("zrange", 1, "one"); client.zadd("zrange", 2, "two"); client.zadd("zrange", 3, "three"); const v = await client.zrange("zrange", 0, 1); assertEquals(v, ["one", "two"]); }); it("testZrangeWithScores", async function testZrangeWithScores() { client.zadd("zrangeWithScores", 1, "one"); client.zadd("zrangeWithScores", 2, "two"); client.zadd("zrangeWithScores", 3, "three"); const v = await client.zrange("zrangeWithScores", 0, 1, { withScore: true, }); assertEquals(v, ["one", "1", "two", "2"]); }); it("testZrevrange", async function testZrevrange() { client.zadd("zrevrange", 1, "one"); client.zadd("zrevrange", 2, "two"); client.zadd("zrevrange", 3, "three"); const v = await client.zrevrange("zrevrange", 0, 1); assertEquals(v, ["three", "two"]); }); it( "testZrevrangeWithScores", async function testZrevrangeWithScores() { client.zadd("zrevrangeWithScores", 1, "one"); client.zadd("zrevrangeWithScores", 2, "two"); client.zadd("zrevrangeWithScores", 3, "three"); const v = await client.zrevrange("zrevrangeWithScores", 0, 1, { withScore: true, }); assertEquals(v, ["three", "3", "two", "2"]); }, ); it("testZrangebyscore", async function testZrangebyscore() { client.zadd("zrangebyscore", 2, "m1"); client.zadd("zrangebyscore", 5, "m2"); client.zadd("zrangebyscore", 8, "m3"); client.zadd("zrangebyscore", 10, "m4"); const v = await client.zrangebyscore("zrangebyscore", 3, 9); assertEquals(v, ["m2", "m3"]); }); it( "testZrangebyscoreWithScores", async function testZrangebyscoreWithScores() { client.zadd("zrangebyscoreWithScores", 2, "m1"); client.zadd("zrangebyscoreWithScores", 5, "m2"); client.zadd("zrangebyscoreWithScores", 8, "m3"); client.zadd("zrangebyscoreWithScores", 10, "m4"); const v = await client.zrangebyscore("zrangebyscoreWithScores", 3, 9, { withScore: true, }); assertEquals(v, ["m2", "5", "m3", "8"]); }, ); it("testZrevrangebyscore", async function testZrevrangebyscore() { client.zadd("zrevrangebyscore", 2, "m1"); client.zadd("zrevrangebyscore", 5, "m2"); client.zadd("zrevrangebyscore", 8, "m3"); client.zadd("zrevrangebyscore", 10, "m4"); const v = await client.zrevrangebyscore("zrevrangebyscore", 9, 4); assertEquals(v, ["m3", "m2"]); }); it("testZrevrangebyscore", async function testZrevrangebyscore() { client.zadd("zrevrangebyscoreWithScores", 2, "m1"); client.zadd("zrevrangebyscoreWithScores", 5, "m2"); client.zadd("zrevrangebyscoreWithScores", 8, "m3"); client.zadd("zrevrangebyscoreWithScores", 10, "m4"); const v = await client.zrevrangebyscore( "zrevrangebyscoreWithScores", 9, 4, { withScore: true, }, ); assertEquals(v, ["m3", "8", "m2", "5"]); }); } ================================================ FILE: tests/commands/stream.ts ================================================ import type { Redis } from "../../mod.ts"; import { ErrorReplyError } from "../../mod.ts"; import { parseXId } from "../../stream.ts"; import { delay } from "../../deps/std/async.ts"; import { assert, assertEquals, assertNotEquals, assertRejects, } from "../../deps/std/assert.ts"; import { afterAll, beforeAll, it } from "../../deps/std/testing.ts"; import type { Connector, TestServer } from "../test_util.ts"; export function streamTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); const rnum = () => Math.floor(Math.random() * 1000); const randomStream = () => `test-deno-${new Date().getTime()}-${rnum()}${rnum()}${rnum()}`; const cleanupStream = async (client: Redis, ...keys: string[]) => { await Promise.all(keys.map((key) => client.xtrim(key, { elements: 0 }))); }; const withConsumerGroup = async ( fn: (stream: string, group: string) => Promise, ) => { const rn = Math.floor(Math.random() * 1000); const stream = randomStream(); const group = `test-group-${rn}`; const created = await client.xgroupCreate(stream, group, "$", true); assertEquals(created, "OK"); await fn(stream, group); assertEquals(await client.xgroupDestroy(stream, group), 1); }; it("xadd", async () => { const key = randomStream(); const v = await client.xadd(key, "*", { cat: "what", dog: "who", duck: "when", }); assert(v != null); await cleanupStream(client, key); }); it("xadd maxlen", async () => { const key = randomStream(); const v = await client.xadd( key, "*", { cat: "meow", dog: "woof", duck: "quack" }, { elements: 10 }, ); assert(v != null); const x = await client.xadd( key, "*", { cat: "oo", dog: "uu", duck: "pp" }, { approx: false, elements: 10 }, ); assert(x != null); await cleanupStream(client, key); }); it("xreadgroup multiple streams", async () => { await withConsumerGroup(async (key, group) => { const key2 = randomStream(); const created = await client.xgroupCreate(key2, group, "$", true); assertEquals(created, "OK"); await Promise.all([ client.xadd(key, "*", { a: 1, b: 2 }), client.xadd(key, "*", { a: 6, b: 7 }), client.xadd(key2, "*", { c: "three", d: "four" }), ]); const reply = await client.xreadgroup( [ [key, ">"], [key2, ">"], ], { group, consumer: "any" }, ); assertEquals(reply.length, 2); assertEquals(reply[0].key, key); assertEquals(reply[0].messages.length, 2); assertEquals(reply[1].key, key2); assertEquals(reply[1].messages.length, 1); // first stream cleaned up by withConsumerGroup await cleanupStream(client, key2); }); }); it("xread", async () => { const key = randomStream(); const a = await client.xadd( key, 1000, // epoch millis only, converts to "1000-0" for the low-level interface to redis { cat: "moo", dog: "honk", duck: "yodel" }, { elements: 10 }, ); assert(a != null); const key2 = randomStream(); await client.xadd( key2, [1000, 0], // You may enter the ID as a numeric pair { air: "ball", friend: "table" }, { elements: 10 }, ); const exampleMap = { air: "horn", friend: "fiend", }; const c = await client.xadd(key2, [1001, 1], exampleMap, { elements: 10 }); assert(c != null); const xid = 0; const v = await client.xread( [ { key, xid }, { key: key2, xid }, ], { block: 5000, count: 500 }, ); assert(v != null); const expectedAnimals = { cat: "moo", dog: "honk", duck: "yodel", }; const expectedWeird = { air: "ball", friend: "table", }; const expectedOdd = { air: "horn", friend: "fiend", }; assertEquals(v, [ { key, messages: [ { xid: parseXId("1000-0"), fieldValues: expectedAnimals, }, ], }, { key: key2, messages: [ { xid: parseXId("1000-0"), fieldValues: expectedWeird }, { xid: parseXId("1001-1"), fieldValues: expectedOdd }, ], }, ]); await cleanupStream(client, key, key2); }); it("xread manage empty stream on timeout", async () => { const key = randomStream(); const [stream] = await client.xread([{ key, xid: 0 }], { block: 1, }); assertEquals(stream, undefined); }); it("xgrouphelp", async () => { const helpText = await client.xgroupHelp(); assert(helpText.length > 4); assert(helpText[0].length > 10); }); it("xgroup create and destroy", async () => { const groupName = "test-group"; const key = randomStream(); const created = await client.xgroupCreate(key, groupName, "$", true); assertEquals(created, "OK"); await assertRejects( async () => { await client.xgroupCreate(key, groupName, 0, true); }, ErrorReplyError, "-BUSYGROUP Consumer Group name already exists", ); assertEquals(await client.xgroupDestroy(key, groupName), 1); }); it("xgroup setid and delconsumer", async () => { const key = randomStream(); const group = "test-group"; const consumer = "test-consumer"; const created = await client.xgroupCreate(key, group, "$", true); assertEquals(created, "OK"); const addedId = await client.xadd(key, "*", { anyfield: "anyval" }); assert(addedId); // must read from a given stream to create the // consumer const xid = ">"; const data = await client.xreadgroup([{ key, xid }], { group, consumer }); assertEquals(data.length, 1); assertEquals(await client.xgroupSetID(key, group, 0), "OK"); assertEquals(await client.xgroupDelConsumer(key, group, consumer), 1); await cleanupStream(client, key); }); it("xreadgroup but no ack", async () => { const key = randomStream(); const group = "test-group"; const created = await client.xgroupCreate(key, group, "$", true); assertEquals(created, "OK"); const addedId = await client.xadd(key, "*", { anyfield: "anyval" }); assert(addedId); const xid = ">"; const dataOut = await client.xreadgroup([{ key, xid }], { group, consumer: "test-consumer", }); assertEquals(dataOut.length, 1); const actualFirstStream = dataOut[0]; assertEquals(actualFirstStream.key, key); assertEquals(actualFirstStream.messages[0].xid, addedId); assertEquals(actualFirstStream.messages.length, 1); assertEquals( actualFirstStream.messages[0].fieldValues["anyfield"], "anyval", ); // > symbol does NOT cause automatic acknowledgement by Redis const ackSize = await client.xack(key, group, addedId); assertEquals(ackSize, 1); assertEquals(await client.xgroupDestroy(key, group), 1); await cleanupStream(client, key); }); it("xack", async () => { const key = randomStream(); const group = "test-group"; const created = await client.xgroupCreate(key, group, "$", true); assertEquals(created, "OK"); const addedId = await client.xadd(key, "*", { anyfield: "anyval" }); assert(addedId); const xid = ">"; // read but DO NOT auto-ack, which places // the message on the PEL await client.xreadgroup([{ key, xid }], { group, consumer: "test-consumer", }); const acked = await client.xack(key, group, addedId); assertEquals(acked, 1); assertEquals(await client.xgroupDestroy(key, group), 1); await cleanupStream(client, key); }); it("xadd with map then xread", async () => { const m = new Map(); m.set("zoo", "theorize"); m.set("gable", "train"); const key = randomStream(); const addedId = await client.xadd(key, "*", m); assert(addedId !== null); // one millis before now const xid = addedId.unixMs - 1; const v = await client.xread([{ key, xid }], { block: 5000, count: 500 }); assert(v != null); const expectedMap = { zoo: "theorize", gable: "train", }; assertEquals(v, [ { key, messages: [ { xid: addedId, fieldValues: expectedMap, }, ], }, ]); await cleanupStream(client, key); }); it("xadd with maxlen on map then xread", async () => { const mmm = new Map(); mmm.set("hop", "4"); mmm.set("blip", "5"); const key = randomStream(); const addedId = await client.xadd(key, "*", mmm, { elements: 8 }); assert(addedId !== null); const justBefore = addedId.unixMs - 1; const v = await client.xread([{ key, xid: justBefore }], { block: 5000, count: 500, }); assert(v != null); const expectedMap = { hop: "4", blip: "5", }; assertEquals(v, [ { key, messages: [{ xid: addedId, fieldValues: expectedMap }] }, ]); await cleanupStream(client, key); }); it("xdel", async () => { const key = randomStream(); const id0 = await client.xadd(key, "*", { foo: "bar" }, { elements: 10 }); const id1 = await client.xadd(key, "*", { foo: "baz" }, { elements: 10 }); const id2 = await client.xadd(key, "*", { foo: "qux" }, { elements: 10 }); const v = await client.xdel(key, id0, id1, id2); assert(v === 3); await cleanupStream(client, key); }); it("xlen", async () => { const key = randomStream(); await client.xadd(key, "*", { foo: "qux" }, { elements: 5 }); await client.xadd(key, "*", { foo: "bux" }, { elements: 5 }); const v = await client.xlen(key); assert(v === 2); await cleanupStream(client, key); }); it("unique message per consumer", async () => { await withConsumerGroup(async (key, group) => { const addedIds = []; const c0 = "consumer-0"; const c1 = "consumer-1"; const c2 = "consumer-2"; for (const consumer of [c0, c1, c2]) { const payload = `data-for-${consumer}`; const a = await client.xadd(key, "*", { target: payload }); assert(a); addedIds.push(a); // This special ID means that you want all // "new" messages in the stream. const xid = ">"; const data = await client.xreadgroup([{ key, xid }], { group, consumer, }); assertEquals(data[0].messages.length, 1); assertEquals(data[0].messages[0].fieldValues["target"], payload); } await cleanupStream(client, key); }); }); it("broadcast pattern, all groups read their own version of the stream", async () => { const key = randomStream(); const group0 = "tg0"; const group1 = "tg1"; const group2 = "tg2"; const groups = [group0, group1, group2]; for (const g of groups) { const created = await client.xgroupCreate(key, g, "$", true); assertEquals(created, "OK"); } const addedIds = []; let msgCount = 0; for (const group of groups) { const payload = `data-${msgCount}`; const a = await client.xadd(key, "*", { target: payload }); assert(a); addedIds.push(a); msgCount++; const consumer = "someconsumer"; const xid = ">"; const data = await client.xreadgroup([{ key, xid }], { group, consumer, }); // each group should see ALL the messages // that have been emitted const toCheck = data[0].messages; assertEquals(toCheck.length, msgCount); } for (const g of groups) { assertEquals(await client.xgroupDestroy(key, g), 1); } await cleanupStream(client, key); }); it("xrange and xrevrange", async () => { const key = randomStream(); const firstId = await client.xadd(key, "*", { f: "v0" }); const basicResult = await client.xrange(key, "-", "+"); assertEquals(basicResult.length, 1); assertEquals(basicResult[0].xid, firstId); assertEquals(basicResult[0].fieldValues["f"], "v0"); const secondId = await client.xadd(key, "*", { f: "v1" }); const revResult = await client.xrevrange(key, "+", "-"); assertEquals(revResult.length, 2); assertEquals(revResult[0].xid, secondId); assertEquals(revResult[0].fieldValues["f"], "v1"); assertEquals(revResult[1].xid, firstId); assertEquals(revResult[1].fieldValues["f"], "v0"); // count should limit results const lim = await client.xrange(key, "-", "+", 1); assertEquals(lim.length, 1); const revLim = await client.xrevrange(key, "+", "-", 1); assertEquals(revLim.length, 1); await cleanupStream(client, key); }); it("xclaim and xpending, all options", async () => { await withConsumerGroup(async (key, group) => { // xclaim test basic idea: // 1. add messages to a group // 2. then xreadgroup needs to define a consumer and read pending // messages without acking them // 3. then we need to sleep 5ms and call xpending // 4. from here we should be able to claim message // past the idle time and read them from a different consumer await Promise.all([ client.xadd(key, 1000, { field: "foo" }), client.xadd(key, 2000, { field: "bar" }), ]); const initialConsumer = "someone"; const firstReply = await client.xreadgroup([{ key, xid: ">" }], { group, consumer: initialConsumer, }); const firstPending = await client.xpending(key, group); await delay(5); assertEquals(firstPending.count, 2); assertNotEquals(firstPending.startId, firstPending.endId); assertEquals(firstPending.consumers.length, 1); assertEquals(firstPending.consumers[0].name, "someone"); assertEquals(firstPending.consumers[0].pending, 2); const minIdleTime = 4; // minimum options const claimingConsumer = "responsible-process"; const firstClaimed = await client.xclaim( key, { group, consumer: claimingConsumer, minIdleTime }, 1000, 2000, ); assert(firstClaimed.kind === "messages"); assertEquals(firstClaimed.messages.length, 2); assertEquals(firstClaimed.messages[0].fieldValues, { field: "foo" }); assertEquals(firstClaimed.messages[1].fieldValues, { field: "bar" }); // ACK these messages so we can try XPENDING/XCLAIM // on a new batch await client.xack( key, group, ...firstReply[0].messages.map((m) => m.xid), ); // Let's write some more messages and try // other formats of XPENDING/XCLAIM await Promise.all([ client.xadd(key, 3000, { field: "foo" }), client.xadd(key, [3000, 1], { field: "bar" }), client.xadd(key, [3000, 2], { field: "baz" }), ]); const secondReply = await client.xreadgroup([{ key, xid: ">" }], { group, consumer: initialConsumer, }); // take a short nap and increase the lastDeliveredMs await delay(5); // try another form of xpending: counts for all consumers (we have only one) const secondPending = await client.xpendingCount(key, group, { start: "-", end: "+", count: 10, }); assertEquals(secondPending.length, 3); for (const info of secondPending) { assertEquals(info.owner, "someone"); assert(info.lastDeliveredMs > 4); // We called XREADGROUP so it was delivered once // (but not acknowledged yet!) assertEquals(info.timesDelivered, 1); } // the output for justIDs will have a different shape const secondClaimedXIds = await client.xclaim( key, { group, consumer: claimingConsumer, minIdleTime, justXId: true }, [3000, 0], [3000, 1], [3000, 2], ); assert(secondClaimedXIds.kind === "justxid"); assertEquals(secondClaimedXIds.xids, [ { unixMs: 3000, seqNo: 0 }, { unixMs: 3000, seqNo: 1 }, { unixMs: 3000, seqNo: 2 }, ]); // ACK these messages so we can try XPENDING/XCLAIM // on a new batch await client.xack( key, group, ...secondReply[0].messages.map((m) => m.xid), ); // We'll try one other set of options // for each of XPENDING and XCLAIM await Promise.all([ client.xadd(key, 4000, { field: "woof", farm: "chicken" }), client.xadd(key, 5000, { field: "bop", farm: "duck" }), ]); await client.xreadgroup([{ key, xid: ">" }], { group, consumer: initialConsumer, }); // This record won't be included in the filtered // form of XPENDING, below. await client.xadd(key, "*", { field: "no" }); await client.xreadgroup([{ key, xid: ">" }], { group, consumer: "weird-interloper", }); await delay(5); // try another form of xpending: counts // efficiently filtered down to a single consumer. // We expect to see two of the three outstanding // messages here, since one was claimed by // weird-interloper. const thirdPending = await client.xpendingCount( key, group, { start: "-", end: "+", count: 10 }, "someone", ); assertEquals(thirdPending.length, 2); for (const info of thirdPending) { assertEquals(info.owner, "someone"); assert(info.lastDeliveredMs > 4); // We called XREADGROUP so it was delivered once // (but not acknowledged yet!) assertEquals(info.timesDelivered, 1); } // make sure all the other options can be passed to redis // without some sort of disaster occurring. const thirdClaimed = await client.xclaim( key, { group, consumer: claimingConsumer, minIdleTime, retryCount: 6, force: true, }, 4000, 5000, ); assert(thirdClaimed.kind === "messages"); assertEquals(thirdClaimed.messages.length, 2); assertEquals(thirdClaimed.messages[0].fieldValues, { field: "woof", farm: "chicken", }); assertEquals(thirdClaimed.messages[1].fieldValues, { field: "bop", farm: "duck", }); }); }); it("xinfo", async () => { await withConsumerGroup(async (key, group) => { await client.xadd(key, 1, { hello: "no" }); await client.xadd(key, 2, { hello: "yes" }); const basicStreamInfo = await client.xinfoStream(key); assertEquals(basicStreamInfo.length, 2); assertEquals(basicStreamInfo.groups, 1); assert(basicStreamInfo.radixTreeKeys > 0); assert(basicStreamInfo.radixTreeNodes > 0); assertEquals(basicStreamInfo.lastGeneratedId, { unixMs: 2, seqNo: 0 }); assertEquals(basicStreamInfo.firstEntry, { xid: { unixMs: 1, seqNo: 0 }, fieldValues: { hello: "no" }, }); assertEquals(basicStreamInfo.lastEntry, { xid: { unixMs: 2, seqNo: 0 }, fieldValues: { hello: "yes" }, }); // Let's do an XREADGROUP so that we see some entries in the PEL const _ = client.xreadgroup([[key, ">"]], { group, consumer: "someone" }); const fullStreamInfo = await client.xinfoStreamFull(key); assertEquals(fullStreamInfo.length, 2); assert(fullStreamInfo.radixTreeKeys > 0); assert(fullStreamInfo.radixTreeNodes > 0); assertEquals(fullStreamInfo.groups.length, 1); assertEquals(fullStreamInfo.groups[0].consumers.length, 1); const cc = fullStreamInfo.groups[0].consumers[0]; assertEquals(cc.name, "someone"); assert(cc.seenTime > 0); assertEquals(cc.pelCount, 2); assertEquals(cc.pending.length, 2); for (const msg of cc.pending) { assertEquals(msg.timesDelivered, 1); } assertEquals(fullStreamInfo.entries.length, 2); const limitWithCount = await client.xinfoStreamFull(key, 1); assertEquals(limitWithCount.length, 2); assert(limitWithCount.radixTreeKeys > 0); assert(limitWithCount.radixTreeNodes > 0); assertEquals(limitWithCount.groups.length, 1); assertEquals(limitWithCount.groups[0].consumers.length, 1); const c = limitWithCount.groups[0].consumers[0]; assertEquals(c.name, "someone"); assert(c.seenTime > 0); assertEquals(c.pelCount, 2); // The COUNT option limits this array to a single entry! assertEquals(c.pending.length, 1); for (const msg of c.pending) { assertEquals(msg.timesDelivered, 1); } // The COUNT option limits this array to a single entry! assertEquals(limitWithCount.entries.length, 1); // Let's make another group and see more stats await client.xgroupCreate(key, "newgroup", "$", true); const groupInfos = await client.xinfoGroups(key); assertEquals(groupInfos.length, 2); const newGroup = groupInfos.find((g) => g.name === "newgroup"); const oldGroup = groupInfos.find((g) => g.name === group); assert(newGroup); assert(oldGroup); assertEquals(oldGroup.pending, 2); assertEquals(newGroup.pending, 0); // Add one more record and read it with a new consumer, // so that we can check the parsing of deno-redis xinfo_consumers await client.xadd(key, "*", { hello: "maybe" }); await client.xreadgroup([[key, ">"]], { group, consumer: "newbie" }); // Increase the idle time by falling asleep await delay(2); const consumerInfos = await client.xinfoConsumers(key, group); assertEquals(consumerInfos.length, 2); const newConsumer = consumerInfos.find((c) => c.name === "newbie"); const oldConsumer = consumerInfos.find((c) => c.name === "someone"); assert(newConsumer); assert(oldConsumer); assert(newConsumer.idle > 1); assert(oldConsumer.idle > 1); // New consumer read one message with ">" assertEquals(newConsumer.pending, 1); // Old consumer read two messages with ">" assertEquals(oldConsumer.pending, 2); assertEquals(await client.xgroupDestroy(key, "newgroup"), 1); }); }); } ================================================ FILE: tests/commands/string.ts ================================================ import { assertEquals, assertGreater, assertLessOrEqual, } from "../../deps/std/assert.ts"; import type { IsExact, IsNullable } from "../../deps/std/testing.ts"; import { afterAll, assertType, beforeAll, beforeEach, describe, it, } from "../../deps/std/testing.ts"; import { type Connector, type TestServer, usesRedisVersion, } from "../test_util.ts"; import type { Redis } from "../../mod.ts"; export function stringTests( connect: Connector, getServer: () => TestServer, ): void { let client!: Redis; beforeAll(async () => { const server = getServer(); client = await connect({ hostname: "127.0.0.1", port: server.port }); }); afterAll(() => client.close()); beforeEach(async () => { await client.flushdb(); }); it("append", async () => { await client.set("key", "foo"); const rep = await client.append("key", "bar"); assertEquals(rep, 6); const v = await client.get("key"); assertEquals(v, "foobar"); }); it("bitcount", async () => { await client.set("key", "foo"); // 01100110 01101111 01101111 const v = await client.bitcount("key"); assertEquals(v, 16); }); it("bitfieldWithoutOperations", async () => { await client.set("key", "test"); const v = await client.bitfield("key"); assertEquals(v, []); }); it("bitfield with opts", async () => { await client.set("key", "4660"); const v = await client.bitfield("key", { get: { type: "u8", offset: 0 }, set: { type: "i5", offset: 1, value: 0 }, incrby: { type: "u16", offset: 2, increment: 2 }, }); assertEquals(v, [52, 13, 218]); }); it("bitfield with overflow", async () => { const v = await client.bitfield("key", { overflow: "FAIL", }); assertEquals(v, []); }); it("bitop", async () => { await client.set("key1", "foo"); // 01100110 01101111 01101111 await client.set("key2", "bar"); // 01100010 01100001 01110010 await client.bitop("AND", "dest", "key1", "key2"); const v = await client.get("dest"); assertEquals(v, "bab"); // 01100010 01100001 01100000 }); it("bitpos", async () => { await client.set("key", "2"); // 00110010 assertEquals(await client.bitpos("key", 0), 0); assertEquals(await client.bitpos("key", 1), 2); }); it("decr", async () => { const rep = await client.decr("key"); assertEquals(rep, -1); assertEquals(await client.get("key"), "-1"); }); it("decby", async () => { const rep = await client.decrby("key", 101); assertEquals(rep, -101); assertEquals(await client.get("key"), "-101"); }); it("getWhenNil", async () => { const hoge = await client.get("none"); assertEquals(hoge, null); }); it("getbit", async () => { await client.set("key", "3"); // 00110011 assertEquals(await client.getbit("key", 0), 0); assertEquals(await client.getbit("key", 2), 1); }); it("getrange", async () => { await client.set("key", "Hello world!"); const v = await client.getrange("key", 6, 10); assertEquals(v, "world"); }); it("getset", async function testGetSet() { await client.set("key", "val"); const v = await client.getset("key", "lav"); assertEquals(v, "val"); assertEquals(await client.get("key"), "lav"); }); it("incr", async () => { const rep = await client.incr("key"); assertEquals(rep, 1); assertEquals(await client.get("key"), "1"); }); it("incrby", async () => { const rep = await client.incrby("key", 101); assertEquals(rep, 101); assertEquals(await client.get("key"), "101"); }); it("incrbyfloat", async () => { await client.set("key", "2.1"); const v = await client.incrbyfloat("key", 0.5); assertEquals(v, "2.6"); assertEquals(await client.get("key"), "2.6"); }); it("mget", async () => { await client.set("key1", "val1"); await client.set("key2", "val2"); await client.set("key3", "val3"); const v = await client.mget("key1", "key2", "key3"); assertEquals(v, ["val1", "val2", "val3"]); }); it("mset", async () => { let rep = await client.mset("key1", "foo"); assertEquals(rep, "OK"); rep = await client.mset({ key2: "bar", key3: "baz" }); assertEquals(rep, "OK"); rep = await client.mset(["key4", "bar"], ["key5", "baz"]); assertEquals(rep, "OK"); assertEquals(await client.get("key1"), "foo"); assertEquals(await client.get("key2"), "bar"); assertEquals(await client.get("key3"), "baz"); }); it("msetnx", async () => { let rep1 = await client.msetnx("key1", "foo"); assertEquals(rep1, 1); // All the keys were set. rep1 = await client.msetnx({ key2: "bar" }); assertEquals(rep1, 1); // All the keys were set. rep1 = await client.msetnx(["key4", "bar"], ["key5", "baz"]); assertEquals(rep1, 1); const rep2 = await client.msetnx({ key2: "baz", key3: "qux" }); assertEquals(rep2, 0); // No key was set. assertEquals(await client.get("key1"), "foo"); assertEquals(await client.get("key2"), "bar"); assertEquals(await client.get("key3"), null); }); it("psetex", async () => { const rep = await client.psetex("key1", 1000, "test"); assertEquals(rep, "OK"); assertEquals(await client.get("key1"), "test"); }); describe("set", () => { it("can set a key", async () => { const s = await client.set("key", "fuga你好こんにちは"); assertEquals(s, "OK"); assertType>(true); const fuga = await client.get("key"); assertEquals(fuga, "fuga你好こんにちは"); }); it("supports `Number`", async () => { const s = await client.set("key", 123); assertEquals(s, "OK"); const v = await client.get("key"); assertEquals(v, "123"); }); it("supports `GET` option", async () => { const prev = "foobar"; await client.set("setWithGetOpt", prev); const result = await client.set("setWithGetOpt", "baz", { get: true }); assertEquals(result, prev); assertType>(true); }); it("supports `NX` option", async () => { const prev = "foo"; const v1 = await client.set("setWithNX", prev, { mode: "NX" }); assertEquals(v1, "OK"); assertType>(true); const v2 = await client.set("setWithNX", "bar", { nx: true }); assertEquals(v2, null); assertType>(true); }); it("supports `XX` option", async () => { const v1 = await client.set("setWithXX", "foo", { mode: "XX" }); assertEquals(v1, null); assertType>(true); const v2 = await client.set("setWithXX", "foo", { xx: true }); assertEquals(v2, null); assertType>(true); }); it("supports `EXAT` option", async () => { const date = new Date(); date.setDate(date.getDate() + 1); await client.set("setWithEXAT", "foo", { exat: Math.floor(date.valueOf() / 1000), }); const ttl = await client.ttl("setWithEXAT"); const oneDay = 86400; assertLessOrEqual(ttl, oneDay); assertGreater(ttl, oneDay - 3); }); it("supports `PXAT` option", async () => { const date = new Date(); date.setDate(date.getDate() + 1); await client.set("setWithPXAT", "bar", { pxat: date.valueOf() }); const ttl = await client.ttl("setWithPXAT"); const oneDay = 86400; assertLessOrEqual(ttl, oneDay); assertGreater(ttl, oneDay - 3); }); }); it("setbit", async () => { await client.set("key", "2"); // 00110010 assertEquals( 0, await client.setbit("key", 1, "1"), // 01110010 ); assertEquals( 1, await client.setbit("key", 3, "0"), // 01100010 => b ); const v = await client.get("key"); assertEquals(v, "b"); }); it("setex", async () => { const rep = await client.setex("key", 1, "test"); assertEquals(rep, "OK"); assertEquals(await client.get("key"), "test"); }); it("setnx", async () => { assertEquals(await client.setnx("key", "foo"), 1); assertEquals(await client.setnx("key", "bar"), 0); const v = await client.get("key"); assertEquals(v, "foo"); }); it("setrange", async () => { await client.set("key", "Hello, Deno!"); const rep = await client.setrange("key", 7, "Redis!"); assertEquals(rep, 13); const v = await client.get("key"); assertEquals(v, "Hello, Redis!"); }); it("stralgo", { // NOTE(#454): STRALGO has been dropped ignore: !usesRedisVersion("6"), }, async () => { await client.set("a", "Hello"); await client.set("b", "Deno!"); const matches = [ [ [4, 4], [3, 3], ], [ [1, 1], [1, 1], ], ]; const matchesWithLen = [ [ [4, 4], [3, 3], 1, ], [ [1, 1], [1, 1], 1, ], ]; assertEquals(await client.stralgo("LCS", "KEYS", "a", "b"), "eo"); assertEquals( await client.stralgo("LCS", "KEYS", "a", "b", { len: true }), 2, ); assertEquals( await client.stralgo("LCS", "KEYS", "a", "b", { idx: true }), ["matches", matches, "len", 2], ); assertEquals( await client.stralgo( "LCS", "KEYS", "a", "b", { idx: true, withmatchlen: true }, ), ["matches", matchesWithLen, "len", 2], ); assertEquals( await client.stralgo( "LCS", "KEYS", "a", "b", { idx: true, minmatchlen: 2 }, ), ["matches", [], "len", 2], ); assertEquals( await client.stralgo("LCS", "STRINGS", "Hello", "Deno!"), "eo", ); assertEquals( await client.stralgo("LCS", "STRINGS", "Hello", "Deno!", { len: true }), 2, ); }); it("strlen", async () => { await client.set("key", "foobar"); const v = await client.strlen("key"); assertEquals(v, 6); }); } ================================================ FILE: tests/commands_test.ts ================================================ import { nextPort, startRedis, stopRedis } from "./test_util.ts"; import type { TestServer } from "./test_util.ts"; import { aclTests } from "./commands/acl.ts"; import { connectionTests } from "./commands/connection.ts"; import { generalTests } from "./commands/general.ts"; import { geoTests } from "./commands/geo.ts"; import { hashTests } from "./commands/hash.ts"; import { hyperloglogTests } from "./commands/hyper_loglog.ts"; import { keyTests } from "./commands/key.ts"; import { listTests } from "./commands/list.ts"; import { pipelineTests } from "./commands/pipeline.ts"; import { pubsubTests } from "./commands/pubsub.ts"; import { setTests } from "./commands/set.ts"; import { zsetTests } from "./commands/sorted_set.ts"; import { scriptTests } from "./commands/script.ts"; import { streamTests } from "./commands/stream.ts"; import { stringTests } from "./commands/string.ts"; import { latencyTests } from "./commands/latency.ts"; import { resp3Tests } from "./commands/resp3.ts"; import { connect } from "../redis.ts"; import { connect as connectWebStreamsConnection } from "../experimental/web_streams_connection/mod.ts"; import { afterAll, beforeAll, describe } from "../deps/std/testing.ts"; describe("commands", () => { let port!: number; let server!: TestServer; beforeAll(async () => { port = nextPort(); server = await startRedis({ port }); }); afterAll(() => stopRedis(server)); const getServer = () => server; for ( const [kind, connector] of [ ["deno_streams connection", connect] as const, [ "experimental web_streams connection", connectWebStreamsConnection, ] as const, ] ) { describe(kind, () => { describe("acl", () => aclTests(connector, getServer)); describe("connection", () => connectionTests(connector, getServer)); describe("general", () => generalTests(connector, getServer)); describe("geo", () => geoTests(connector, getServer)); describe("hash", () => hashTests(connector, getServer)); describe("hyperloglog", () => hyperloglogTests(connector, getServer)); describe("key", () => keyTests(connector, getServer)); describe("list", () => listTests(connector, getServer)); describe("pipeline", () => pipelineTests(connector, getServer)); describe("pubsub", () => pubsubTests(connector, getServer)); describe("set", () => setTests(connector, getServer)); describe("zset", () => zsetTests(connector, getServer)); describe("script", () => scriptTests(connector, getServer)); describe("stream", () => streamTests(connector, getServer)); describe("string", () => stringTests(connector, getServer)); describe("latency", () => latencyTests(connector, getServer)); describe("RESP3", () => resp3Tests(connector, getServer)); }); } }); ================================================ FILE: tests/pool_test.ts ================================================ import type { DefaultPubSubMessageType, RedisSubscription, } from "../subscription.ts"; import { createPoolClient } from "../pool/mod.ts"; import { assert, assertEquals } from "../deps/std/assert.ts"; import { afterAll, beforeAll, describe, it } from "../deps/std/testing.ts"; import type { TestServer } from "./test_util.ts"; import { nextPort, startRedis, stopRedis } from "./test_util.ts"; describe("createPoolClient", () => { let port!: number; let server!: TestServer; beforeAll(async () => { port = nextPort(); server = await startRedis({ port }); }); afterAll(() => stopRedis(server)); it("supports distributing commands to pooled connections", async () => { const client = await createPoolClient({ connection: { hostname: "127.0.0.1", port, }, }); try { const blpopPromise = client.blpop(500, "list"); setTimeout(() => { client.lpush("list", "foo"); }, 100); const existsPromise = client.exists("list"); const replies = await Promise.all([ blpopPromise, existsPromise, ]); assertEquals( replies, [["list", "foo"], 0], "BLPOP should not block subsequent EXISTS", ); } finally { await client.flushdb(); client.close(); } }); it("supports Pub/Sub", async () => { const client = await createPoolClient({ connection: { hostname: "127.0.0.1", port, }, }); let subscription: RedisSubscription | null = null; try { const channel = "pool_test"; subscription = await client.subscribe(channel); const payload = "foobar"; await client.publish(channel, payload); const iter = subscription.receive(); const message = await iter.next(); assert(!message.done); assertEquals(message.value, { channel, message: payload, }); } finally { subscription?.close(); await client.flushdb(); client.close(); } }); }); ================================================ FILE: tests/reconnect_test.ts ================================================ import { assert, assertEquals, assertInstanceOf, assertStringIncludes, } from "../deps/std/assert.ts"; import { beforeAll, describe, it } from "../deps/std/testing.ts"; import { newClient, nextPort, startRedis, stopRedis, withTimeout, } from "./test_util.ts"; describe("auto reconnection", () => { let port!: number; beforeAll(() => { port = nextPort(); }); it( "supports auto reconnection", withTimeout(10000, async () => { await using server = await startRedis({ port }); using client = await newClient({ hostname: "127.0.0.1", port }); assertEquals(await client.ping(), "PONG"); await stopRedis(server); let reconnectingFired = 0; client.addEventListener("reconnecting", (e) => { reconnectingFired++; assertInstanceOf(e, CustomEvent); }); let connectFired = 0; client.addEventListener("connect", (e) => { connectFired++; assertInstanceOf(e, CustomEvent); }); await using server2 = await startRedis({ port }); assertEquals(await client.ping(), "PONG"); client.close(); await stopRedis(server2); assertEquals(reconnectingFired, 1); assertEquals(connectFired, 1); }), ); it( "supports auto reconnection with db spec", withTimeout(10000, async () => { // Regression test for https://github.com/denodrivers/redis/issues/430 await using server = await startRedis({ port }); using client = await newClient({ hostname: "127.0.0.1", port, db: 1, backoff: () => 100, }); assertEquals(await client.ping(), "PONG"); await stopRedis(server); await using server2 = await startRedis({ port }); assertEquals(await client.ping(), "PONG"); const clientInfo = await client.clientInfo(); assert(clientInfo); assertStringIncludes(clientInfo, "db=1"); client.close(); await stopRedis(server2); }), ); }); ================================================ FILE: tests/server/redis.conf ================================================ # Base configuration daemonize no appendonly yes cluster-node-timeout 30000 maxclients 1001 ================================================ FILE: tests/test_util.ts ================================================ import type { Redis, RedisConnectOptions } from "../mod.ts"; import { connect } from "../mod.ts"; import { delay } from "../deps/std/async.ts"; export type Connector = typeof connect; interface Logger { info(message: string): void; } export interface TestServer { path: string; port: number; process: Deno.ChildProcess; logger: Logger; [Symbol.asyncDispose](): Promise; } const encoder = new TextEncoder(); export async function startRedis({ port = 6379, clusterEnabled = false, makeClusterConfigFile = false, logger = console, }): Promise { const path = tempPath(String(port)); if (!(await exists(path))) { await Deno.mkdir(path); } // Setup redis.conf const destPath = `${path}/redis.conf`; let config = await Deno.readTextFile("tests/server/redis.conf"); config += `dir ${path}\nport ${port}\n`; if (clusterEnabled) { config += "cluster-enabled yes\n"; if (makeClusterConfigFile) { const clusterConfigFile = `${path}/cluster.conf`; config += `cluster-config-file ${clusterConfigFile}`; } } await Deno.writeFile(destPath, encoder.encode(config)); // Start a redis server logger.info(`Start a redis server at port ${port}`); const process = new Deno.Command("redis-server", { args: [`${path}/redis.conf`], stdin: "null", stdout: "null", stderr: "piped", }).spawn(); await waitForPort(port); const testServer = { path, port, process, logger, [Symbol.asyncDispose]: () => { return stopRedis(testServer); }, }; return testServer; } export async function stopRedis(server: TestServer): Promise { try { await Deno.remove(server.path, { recursive: true }); } catch (error) { if (!(error instanceof Deno.errors.NotFound)) { throw error; } } server.logger.info(`Stop a redis server running in port ${server.port}`); await ensureTerminated(server.process); } export async function ensureTerminated( process: Deno.ChildProcess, ): Promise { try { await process.stderr.cancel(); process.kill("SIGKILL"); await process.status; } catch (error) { const alreadyKilled = error instanceof TypeError && error.message === "Child process has already terminated"; if (alreadyKilled) { return; } throw error; } } export function newClient(opt: RedisConnectOptions): Promise { return connect(opt); } async function exists(path: string): Promise { try { await Deno.stat(path); return true; } catch (err) { if (err instanceof Deno.errors.NotFound) { return false; } throw err; } } let currentPort = 7000; export function nextPort(): number { return currentPort++; } async function waitForPort(port: number): Promise { let retries = 0; const maxRetries = 5; while (true) { try { const conn = await Deno.connect({ port }); conn.close(); break; } catch (e) { retries++; if (retries === maxRetries) { throw e; } await delay(200); } } } function tempPath(fileName: string): string { const url = new URL(`./tmp/${fileName}`, import.meta.url); return url.pathname; } export function usesRedisVersion(version: "6" | "7" | "8"): boolean { const redisVersion = Deno.env.get("REDIS_VERSION"); if (redisVersion == null) return false; return redisVersion.startsWith(`${version}.`) || redisVersion === version; } export function withTimeout( timeout: number, thunk: () => Promise, ): () => Promise { async function thunkWithTimeout(): Promise { const { resolve, reject, promise } = Promise.withResolvers(); const timer = setTimeout(() => { reject(new Error("Timeout")); }, timeout); try { await Promise.race([ thunk().then(() => resolve()), promise, ]); } finally { clearTimeout(timer); } } return thunkWithTimeout; } ================================================ FILE: tests/util_test.ts ================================================ import { parseURL } from "../mod.ts"; import { assertEquals } from "../deps/std/assert.ts"; import { describe, it } from "../deps/std/testing.ts"; describe("util", { permissions: "none", }, () => { describe("parseURL", () => { it("parses basic URL", () => { const options = parseURL("redis://127.0.0.1:7003"); assertEquals(options.hostname, "127.0.0.1"); assertEquals(options.port, 7003); assertEquals(options.tls, false); assertEquals(options.db, undefined); assertEquals(options.name, undefined); assertEquals(options.password, undefined); }); it("parses complex URL", () => { const options = parseURL("rediss://username:password@127.0.0.1:7003/1"); assertEquals(options.hostname, "127.0.0.1"); assertEquals(options.port, 7003); assertEquals(options.tls, true); assertEquals(options.db, 1); assertEquals(options.name, "username"); assertEquals(options.password, "password"); }); it("parses URL with search options", () => { const options = parseURL( "redis://127.0.0.1:7003/?db=2&password=password&ssl=true", ); assertEquals(options.hostname, "127.0.0.1"); assertEquals(options.port, 7003); assertEquals(options.tls, true); assertEquals(options.db, 2); assertEquals(options.name, undefined); assertEquals(options.password, "password"); }); it("checks parameter parsing priority", () => { const options = parseURL( "rediss://username:password@127.0.0.1:7003/1?db=2&password=password2&ssl=false", ); assertEquals(options.hostname, "127.0.0.1"); assertEquals(options.port, 7003); assertEquals(options.tls, true); assertEquals(options.db, 1); assertEquals(options.name, "username"); assertEquals(options.password, "password"); }); }); }); ================================================ FILE: tools/format-benchmark-results.js ================================================ // deno-lint-ignore-file no-console -- This file is a script, not a library module. import { join } from "node:path"; function formatResultsAsMarkdown({ name, results }) { const detailKeys = ["min", "max", "mean", "median"]; const header = ["name", "ops", "margin", ...detailKeys, "samples"]; const rows = results.map((result) => { const { name, ops, margin, details, samples } = result; return [ name, ops, margin, ...detailKeys.map((key) => details[key]), samples, ]; }); const table = [ header, header.map(() => ":---:"), ...rows, ] .map(makeTableRow) .join("\n"); return `## ${name}\n\n${table}\n`; } function makeTableRow(columns) { return `|${columns.join("|")}|`; } const resultsDir = new URL("../tmp/benchmark", import.meta.url); const paths = Array.from(Deno.readDirSync(resultsDir)).map((x) => join(resultsDir.pathname, x.name) ).sort(); for (const path of paths) { const results = JSON.parse(await Deno.readTextFile(path)); const markdown = formatResultsAsMarkdown(results); console.log(markdown); }