Loading CHANGES.md +4 −0 Original line number Diff line number Diff line Loading @@ -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 ------------- Loading packages/redis/src/kv.test.ts +44 −0 Original line number Diff line number Diff line Loading @@ -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(); } }); packages/redis/src/kv.ts +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"; Loading Loading @@ -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"); } } Loading
CHANGES.md +4 −0 Original line number Diff line number Diff line Loading @@ -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 ------------- Loading
packages/redis/src/kv.test.ts +44 −0 Original line number Diff line number Diff line Loading @@ -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(); } });
packages/redis/src/kv.ts +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"; Loading Loading @@ -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"); } }