Unverified Commit 29ec5532 authored by Hong Minhee's avatar Hong Minhee
Browse files

Implement list() method in RedisKvStore

parent 0e2f6faf
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -64,6 +64,10 @@ To be released.

 -  Implemented `list()` method in `PostgresKvStore`.  [[#498]]

### @fedify/redis

 -  Implemented `list()` method in `RedisKvStore`.  [[#498]]


Version 1.9.2
-------------
+44 −0
Original line number Diff line number Diff line
@@ -47,3 +47,47 @@ test("RedisKvStore.delete()", { skip }, async () => {
    redis.disconnect();
  }
});

test("RedisKvStore.list()", { skip }, async () => {
  if (skip) return; // see https://github.com/oven-sh/bun/issues/19412
  const { redis, store } = getRedis();
  try {
    await store.set(["prefix", "a"], "value-a");
    await store.set(["prefix", "b"], "value-b");
    await store.set(["prefix", "nested", "c"], "value-c");
    await store.set(["other", "x"], "value-x");

    const entries: { key: readonly string[]; value: unknown }[] = [];
    for await (const entry of store.list!({ prefix: ["prefix"] })) {
      entries.push({ key: entry.key, value: entry.value });
    }

    assert.strictEqual(entries.length, 3);
    assert(entries.some((e) => e.key[1] === "a" && e.value === "value-a"));
    assert(entries.some((e) => e.key[1] === "b"));
    assert(entries.some((e) => e.key[1] === "nested"));
  } finally {
    await redis.flushdb();
    redis.disconnect();
  }
});

test("RedisKvStore.list() - single element key", { skip }, async () => {
  if (skip) return; // see https://github.com/oven-sh/bun/issues/19412
  const { redis, store } = getRedis();
  try {
    await store.set(["a"], "value-a");
    await store.set(["b"], "value-b");

    const entries: { key: readonly string[]; value: unknown }[] = [];
    for await (const entry of store.list!({ prefix: ["a"] })) {
      entries.push({ key: entry.key, value: entry.value });
    }

    assert.strictEqual(entries.length, 1);
    assert.strictEqual(entries[0].value, "value-a");
  } finally {
    await redis.flushdb();
    redis.disconnect();
  }
});
+63 −1
Original line number Diff line number Diff line
import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
import type {
  KvKey,
  KvStore,
  KvStoreListEntry,
  KvStoreListOptions,
  KvStoreSetOptions,
} from "@fedify/fedify";
import type { Cluster, Redis, RedisKey } from "ioredis";
import { Buffer } from "node:buffer";
import { type Codec, JsonCodec } from "./codec.ts";
@@ -104,4 +110,60 @@ export class RedisKvStore implements KvStore {
    const serializedKey = this.#serializeKey(key);
    await this.#redis.del(serializedKey);
  }

  #deserializeKey(redisKey: string): KvKey {
    const prefixStr = typeof this.#keyPrefix === "string"
      ? this.#keyPrefix
      : new TextDecoder().decode(new Uint8Array(this.#keyPrefix));
    const suffix = redisKey.slice(prefixStr.length);
    return suffix.split("::").map((p) =>
      p.replaceAll("_:", ":")
    ) as unknown as KvKey;
  }

  /**
   * {@inheritDoc KvStore.list}
   * @since 1.10.0
   */
  async *list(
    options: KvStoreListOptions,
  ): AsyncIterable<KvStoreListEntry> {
    const prefixKey = this.#serializeKey(options.prefix);
    const prefixKeyStr = typeof prefixKey === "string"
      ? prefixKey
      : new TextDecoder().decode(new Uint8Array(prefixKey));

    // First, check if the exact prefix key exists
    const exactValue = await this.#redis.getBuffer(prefixKey);
    if (exactValue != null) {
      yield {
        key: options.prefix,
        value: this.#codec.decode(exactValue),
      };
    }

    // Then scan for all keys starting with prefix::
    const pattern = `${prefixKeyStr}::*`;

    let cursor = "0";
    do {
      const [nextCursor, keys] = await this.#redis.scan(
        cursor,
        "MATCH",
        pattern,
        "COUNT",
        100,
      );
      cursor = nextCursor;

      for (const key of keys) {
        const encodedValue = await this.#redis.getBuffer(key);
        if (encodedValue == null) continue;
        yield {
          key: this.#deserializeKey(key),
          value: this.#codec.decode(encodedValue),
        };
      }
    } while (cursor !== "0");
  }
}