From 5fa202863ee90d0143cad4d471446aeef84d4ee7 Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:46:51 +1100 Subject: [PATCH 1/6] feat: add multi-get and multi-delete functionality to CacheService and implement mset in cache levels --- src/cache.service.spec.ts | 66 +++++++++++++++++++ src/cache.service.ts | 52 +++++++++++++++ src/levels/interfaces/cache-level.ts | 9 +++ src/levels/memory/memory.level.ts | 19 +++++- src/levels/redis/redis.level.ts | 14 ++++ src/utils/backfill.utils.ts | 21 ++++++ .../{ => benchmarks}/cache.benchmark.spec.ts | 14 ++-- vitest.config.ts | 2 + 8 files changed, 189 insertions(+), 8 deletions(-) rename tests/{ => benchmarks}/cache.benchmark.spec.ts (95%) diff --git a/src/cache.service.spec.ts b/src/cache.service.spec.ts index 2e8ad24..0cc1bf5 100644 --- a/src/cache.service.spec.ts +++ b/src/cache.service.spec.ts @@ -511,3 +511,69 @@ describe("should handle flushAll across levels", () => { expect(await cache.get(key, null)).toBe(null); }); }); + +describe("Multi-get and Multi-delete functionality", () => { + beforeAll(async () => { + redisContainer = await new RedisContainer("redis:7.2").start(); + client = new Redis(redisContainer.getConnectionUrl()); + redisLevel = new RedisCacheLevel(client); + + cacheService = new CacheService({ + levels: [memoryLevel, redisLevel], + }); + }); + + it("should mget and mdel across multiple levels", async () => { + const cache = new CacheService({ levels: [memoryLevel, redisLevel] }); + const bingo1 = faker.number.bigInt(); + const bingo2 = faker.number.bigInt(); + const bingo3 = faker.number.bigInt(); + + await cache.set("bingo", bingo1); + await cache.set("bingo1", bingo2); + await cache.set("bingo2", bingo3); + const multiValues = await cache.mget([ + "bingo", + "bingo1", + "bingo2", + ]); + + // Compare as strings to handle BigInt/Number serialization differences + expect(multiValues.map(String)).toEqual( + [bingo1, bingo2, bingo3].map(String), + ); + // Now delete them + await cache.mdel(["bingo", "bingo1", "bingo2"]); + + expect( + await cache.mget(["bingo", "bingo1", "bingo2"]), + ).toEqual([null, null, null]); + }); + + it("should retrieve values from redis if memory level misses in mget", async () => { + const cache = new CacheService({ levels: [memoryLevel, redisLevel] }); + const cacheKey = faker.string.alpha(10); + const value = faker.string.alpha(10); + + // Directly set the value in the Redis level (lower level) + await redisLevel.set(cacheKey, value, 3600); + + // Ensure memory level does not have the value initially + expect(await memoryLevel.get(cacheKey)).toBeUndefined(); + + // Now attempt to mget the value via the cache service + const retrievedValues = await cache.mget([cacheKey]); + + expect( + retrievedValues[0], + "Retrieved value should match the original value from Redis", + ).toBe(value); + + // Now check if the value has been backfilled to the memory level (higher level) + const memoryValue = await memoryLevel.get(cacheKey); + expect( + memoryValue, + "Memory cache should have been backfilled with the value", + ).toBe(value); + }); +}); diff --git a/src/cache.service.ts b/src/cache.service.ts index 0d61958..51561d3 100644 --- a/src/cache.service.ts +++ b/src/cache.service.ts @@ -4,6 +4,7 @@ import type { CacheLevel } from "./levels/interfaces/cache-level"; import type { Lockable } from "./levels/interfaces/lockable"; import { backfillLevels, + backfillLevelsWithMultiKeys, backfillVersionedLevels, } from "./utils/backfill.utils"; import { handleGracefully } from "./utils/error.utils"; @@ -28,6 +29,11 @@ export class CacheService { this.versioning = options.versioning ?? false; } + /** + * Flush all cache levels + * Note: Do not use in production as it will clear the entire cache and may lead to performance issues. + * @return void + */ async flushAll(): Promise { await Promise.allSettled( this.levels.map((level) => @@ -35,7 +41,43 @@ export class CacheService { return await level.flushAll(); }, "Failed to flush cache level"), ), + ) + } + + /** + * Loop through cache levels to get the values for the given keys, + * upon failure backfill missing keys + * @param keys - cache keys + * @returns array of cached values or null if not found + */ + async mget( + keys: string[] + ) { + const results: (unknown)[] = new Array(keys.length).fill(null); + const missingKeysIndexes: number[] = []; + const failedLevels: CacheLevel[] = []; + + for (const level of this.levels) { + const levelResults = await level.mget(keys); + + for (let i = 0; i < keys.length; i++) { + const value = levelResults[i]; + if (value !== undefined && value !== null) { + results[i] = value; + } else { + failedLevels.push(level); + missingKeysIndexes.push(i); + } + } + } + + await backfillLevelsWithMultiKeys( + failedLevels, + keys.filter((_, index) => missingKeysIndexes.includes(index)), + results.filter((_, index) => missingKeysIndexes.includes(index)), ); + + return results; } /** @@ -135,6 +177,16 @@ export class CacheService { return newValue as T; } + async mdel(keys: string[]): Promise { + await Promise.allSettled( + this.levels.map((level) => + handleGracefully(async () => { + return await level.mdel(keys); + }, "Failed to mdel keys from cache level"), + ), + ); + } + /** * Set the value for the given key in all cache levels * @param key - cache key diff --git a/src/levels/interfaces/cache-level.ts b/src/levels/interfaces/cache-level.ts index 92b27da..6352c04 100644 --- a/src/levels/interfaces/cache-level.ts +++ b/src/levels/interfaces/cache-level.ts @@ -1,4 +1,13 @@ export interface CacheLevel { + /** + * Store multiple values in the cache. + * @param keys The cache keys. + * @param values The values to cache. + * @param ttl Time to live in seconds. + * @returns The cached values. + */ + mset(keys: string[], values: T[], ttl?: number): Promise; + /** * Retrieve multiple values from the cache. * @param key The cache key. diff --git a/src/levels/memory/memory.level.ts b/src/levels/memory/memory.level.ts index 18d5b81..6d95d55 100644 --- a/src/levels/memory/memory.level.ts +++ b/src/levels/memory/memory.level.ts @@ -52,6 +52,23 @@ export class MemoryCacheLevel triggerMemoryChange(); } + async mset( + keys: string[], + values: T[], + ttl: number = DEFAULT_TTL, + ): Promise { + await Promise.allSettled( + keys.map((key, index) => { + const value = values[index]; + const expiryDate = Date.now() + ttl * 1000; + const storedItem = { value, expiry: expiryDate }; + this.updateStore(key, storedItem); + }), + ); + + return values; + } + /** * Retrieve multiple values from the cache. * @param key The cache key. @@ -63,7 +80,7 @@ export class MemoryCacheLevel const cachedValue = this.store.get(k) as StoredItem | undefined; if (cachedValue === null || cachedValue === undefined) { - results.push(undefined as T); + results.push(cachedValue as T); } else { results.push(cachedValue.value as T); } diff --git a/src/levels/redis/redis.level.ts b/src/levels/redis/redis.level.ts index c366a11..a2da3c2 100644 --- a/src/levels/redis/redis.level.ts +++ b/src/levels/redis/redis.level.ts @@ -32,6 +32,20 @@ export class RedisCacheLevel implements CacheLevel, Lockable { return parseIfJSON(cachedValue); } + async mset(keys: string[], values: T[], ttl = DEFAULT_TTL): Promise { + const pipeline = this.client.pipeline(); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + pipeline.set(key, serializeForRedis(value), "EX", ttl); + } + + await pipeline.exec(); + + return values; + } + async mget(keys: string[]): Promise { const results = await this.client.mget(...keys); const finalResults: T[] = []; diff --git a/src/utils/backfill.utils.ts b/src/utils/backfill.utils.ts index e8bc210..9af93c8 100644 --- a/src/utils/backfill.utils.ts +++ b/src/utils/backfill.utils.ts @@ -24,12 +24,33 @@ export async function backfillVersionedLevels( ); } +export async function backfillLevelsWithMultiKeys( + failedLevels: CacheLevel[], + keys: string[], + values: unknown[], + ttl?: number, +): Promise { + if (failedLevels.length === 0) return; + await Promise.allSettled( + failedLevels.map((failedLevel) => { + return handleGracefully( + () => { + return failedLevel.mset(keys, values, ttl); + }, + "Failed to backfill mset, gracefully continuing with next level.", + false, + ); + }), + ); +} + export async function backfillLevels( failedLevels: CacheLevel[], key: string, value: unknown, ttl?: number, ): Promise { + console.log('backfillLevels called with key:', key); if (failedLevels.length === 0) return; await Promise.allSettled( failedLevels.map((failedLevel) => { diff --git a/tests/cache.benchmark.spec.ts b/tests/benchmarks/cache.benchmark.spec.ts similarity index 95% rename from tests/cache.benchmark.spec.ts rename to tests/benchmarks/cache.benchmark.spec.ts index b39451f..bad88f9 100644 --- a/tests/cache.benchmark.spec.ts +++ b/tests/benchmarks/cache.benchmark.spec.ts @@ -4,14 +4,14 @@ import { } from "@testcontainers/redis"; import { Redis } from "ioredis"; import { afterAll, beforeAll, describe, it } from "vitest"; -import { CacheService } from "../src/cache.service"; +import { CacheService } from "../../src/cache.service"; import { MemoryCacheLevel, RedisCacheLevel, -} from "../src/levels"; -import { FirstExpiringMemoryPolicy } from "../src/policies/first-expiring-memory.policy"; -import { MemoryPercentageLimitStrategy } from "../src/strategies/memory-percentage-limit.strategy"; -import type { StoredHeapItem } from "../src/levels/memory/memory.level"; +} from "../../src/levels"; +import { FirstExpiringMemoryPolicy } from "../../src/policies/first-expiring-memory.policy"; +import { MemoryPercentageLimitStrategy } from "../../src/strategies/memory-percentage-limit.strategy"; +import type { StoredHeapItem } from "../../src/levels/memory/memory.level"; import { type BenchmarkResult, calculateLatencyStats, @@ -22,7 +22,7 @@ import { instrumentRedisCache, populateCache, runBenchmark, -} from "./utilities/benchmark.utilities"; +} from "../utilities/benchmark.utilities"; import { printBenchmarkHeader, printBenchmarkResults, @@ -30,7 +30,7 @@ import { printMemoryEfficiency, printPerformanceComparison, printWritePerformanceComparison, -} from "./utilities/benchmark-output.utilities"; +} from "../utilities/benchmark-output.utilities"; const TOTAL_CALLS = 10000; diff --git a/vitest.config.ts b/vitest.config.ts index 2ac153d..5238aea 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,9 @@ export default defineConfig({ "**/interfaces/**", // Exclude all files in any interfaces directory "**/*.d.ts", // Exclude all type definition files, "dist/**", + "tests/**", ], }, + exclude: ["**/benchmarks/**", "node_modules/**"], }, }); From 03278b245234a6a4015a0ffd78b26d2bc23e1e98 Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:48:54 +1100 Subject: [PATCH 2/6] 1.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11d383c..3071851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cacheforge", - "version": "1.0.6", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cacheforge", - "version": "1.0.6", + "version": "1.1.0", "dependencies": { "@datastructures-js/heap": "^4.3.7", "@sesamecare-oss/redlock": "^1.4.0", diff --git a/package.json b/package.json index a91fc9b..7c5d55b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cacheforge", - "version": "1.0.6", + "version": "1.1.0", "description": "A multi-level caching library for Node.js applications, supporting in-memory and Redis, and custom cache levels.", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", From 53534b0dc4f4ef8a01944b369ce52dab3e153e23 Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:51:13 +1100 Subject: [PATCH 3/6] refactor: clean up code formatting and improve readability in CacheService, MemoryCacheLevel, and backfill utilities --- src/cache.service.spec.ts | 16 +++++++--------- src/cache.service.ts | 12 +++++------- src/levels/memory/memory.level.ts | 4 +++- src/utils/backfill.utils.ts | 2 +- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/cache.service.spec.ts b/src/cache.service.spec.ts index 0cc1bf5..3f4f10c 100644 --- a/src/cache.service.spec.ts +++ b/src/cache.service.spec.ts @@ -532,12 +532,8 @@ describe("Multi-get and Multi-delete functionality", () => { await cache.set("bingo", bingo1); await cache.set("bingo1", bingo2); await cache.set("bingo2", bingo3); - const multiValues = await cache.mget([ - "bingo", - "bingo1", - "bingo2", - ]); - + const multiValues = await cache.mget(["bingo", "bingo1", "bingo2"]); + // Compare as strings to handle BigInt/Number serialization differences expect(multiValues.map(String)).toEqual( [bingo1, bingo2, bingo3].map(String), @@ -545,9 +541,11 @@ describe("Multi-get and Multi-delete functionality", () => { // Now delete them await cache.mdel(["bingo", "bingo1", "bingo2"]); - expect( - await cache.mget(["bingo", "bingo1", "bingo2"]), - ).toEqual([null, null, null]); + expect(await cache.mget(["bingo", "bingo1", "bingo2"])).toEqual([ + null, + null, + null, + ]); }); it("should retrieve values from redis if memory level misses in mget", async () => { diff --git a/src/cache.service.ts b/src/cache.service.ts index 51561d3..c919dbf 100644 --- a/src/cache.service.ts +++ b/src/cache.service.ts @@ -41,19 +41,17 @@ export class CacheService { return await level.flushAll(); }, "Failed to flush cache level"), ), - ) + ); } /** - * Loop through cache levels to get the values for the given keys, + * Loop through cache levels to get the values for the given keys, * upon failure backfill missing keys * @param keys - cache keys * @returns array of cached values or null if not found */ - async mget( - keys: string[] - ) { - const results: (unknown)[] = new Array(keys.length).fill(null); + async mget(keys: string[]) { + const results: unknown[] = new Array(keys.length).fill(null); const missingKeysIndexes: number[] = []; const failedLevels: CacheLevel[] = []; @@ -70,7 +68,7 @@ export class CacheService { } } } - + await backfillLevelsWithMultiKeys( failedLevels, keys.filter((_, index) => missingKeysIndexes.includes(index)), diff --git a/src/levels/memory/memory.level.ts b/src/levels/memory/memory.level.ts index 6d95d55..1d342cc 100644 --- a/src/levels/memory/memory.level.ts +++ b/src/levels/memory/memory.level.ts @@ -63,9 +63,11 @@ export class MemoryCacheLevel const expiryDate = Date.now() + ttl * 1000; const storedItem = { value, expiry: expiryDate }; this.updateStore(key, storedItem); + + return Promise.resolve(); }), ); - + return values; } diff --git a/src/utils/backfill.utils.ts b/src/utils/backfill.utils.ts index 9af93c8..ad95de5 100644 --- a/src/utils/backfill.utils.ts +++ b/src/utils/backfill.utils.ts @@ -50,7 +50,7 @@ export async function backfillLevels( value: unknown, ttl?: number, ): Promise { - console.log('backfillLevels called with key:', key); + console.log("backfillLevels called with key:", key); if (failedLevels.length === 0) return; await Promise.allSettled( failedLevels.map((failedLevel) => { From 09b5af21a1e920d7418aa042d168f123c6fa2270 Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:57:34 +1100 Subject: [PATCH 4/6] feat: implement mset method in CacheService for setting multiple values across cache levels --- src/cache.service.spec.ts | 53 ++++++++++++++++++++++++++++++++++++++- src/cache.service.ts | 34 ++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/cache.service.spec.ts b/src/cache.service.spec.ts index 3f4f10c..5b2ce88 100644 --- a/src/cache.service.spec.ts +++ b/src/cache.service.spec.ts @@ -512,7 +512,7 @@ describe("should handle flushAll across levels", () => { }); }); -describe("Multi-get and Multi-delete functionality", () => { +describe("Multi-get, Multi-delete and multi set functionality", () => { beforeAll(async () => { redisContainer = await new RedisContainer("redis:7.2").start(); client = new Redis(redisContainer.getConnectionUrl()); @@ -574,4 +574,55 @@ describe("Multi-get and Multi-delete functionality", () => { "Memory cache should have been backfilled with the value", ).toBe(value); }); + + it("should set multiple values across levels in mset", async () => { + const cache = new CacheService({ levels: [memoryLevel, redisLevel] }); + const keys = [ + faker.string.alpha(10), + faker.string.alpha(10), + faker.string.alpha(10), + ]; + const values = [ + faker.string.alpha(10), + faker.string.alpha(10), + faker.string.alpha(10), + ]; + + await cache.mset(keys, values, 3600); + + const retrievedValues = await cache.mget(keys); + + expect(retrievedValues).toEqual(values); + + await cache.mdel(keys); + + expect(await cache.mget(keys)).toEqual([null, null, null]); + }); +}); + +describe('CacheService should instantiate with default TTL and Lock TTL values', () => { + it('should use provided defaultTTL and defaultLockTTL values', () => { + const customDefaultTTL = 5000; + const customDefaultLockTTL = 100; + + const cache = new CacheService({ + levels: [memoryLevel, redisLevel], + defaultTTL: customDefaultTTL, + defaultLockTTL: customDefaultLockTTL, + }); + + expect(cache['defaultTTL']).toBe(customDefaultTTL); + expect(cache['defaultLockTTL']).toBe(customDefaultLockTTL); + }); + + it('should fallback to DEFAULT_TTL and DEFAULT_LOCK_TTL if invalid values are provided', () => { + const cache = new CacheService({ + levels: [memoryLevel, redisLevel], + defaultTTL: NaN, + defaultLockTTL: NaN, + }); + + expect(cache['defaultTTL']).toBe(3600); // DEFAULT_TTL + expect(cache['defaultLockTTL']).toBe(30); // DEFAULT_LOCK_TTL + }); }); diff --git a/src/cache.service.ts b/src/cache.service.ts index c919dbf..786f456 100644 --- a/src/cache.service.ts +++ b/src/cache.service.ts @@ -23,10 +23,14 @@ export class CacheService { protected versioning: boolean; constructor(options: CacheServiceOptions) { - this.levels = options.levels; - this.defaultTTL = options.defaultTTL ?? DEFAULT_TTL; - this.defaultLockTTL = options.defaultLockTTL ?? DEFAULT_LOCK_TTL; - this.versioning = options.versioning ?? false; + const { defaultTTL, defaultLockTTL, levels, versioning } = options; + + this.levels = levels; + this.defaultTTL = (typeof defaultTTL === "number" && + !isNaN(defaultTTL)) ? defaultTTL : DEFAULT_TTL; + this.defaultLockTTL = (typeof defaultLockTTL === "number" && + !isNaN(defaultLockTTL)) ? defaultLockTTL : DEFAULT_LOCK_TTL; + this.versioning = versioning ?? false; } /** @@ -44,6 +48,28 @@ export class CacheService { ); } + /** + * Loop through cache levels to set the values for the given keys + * @param keys - cache keys + * @param values - values to cache + * @param ttl - time to live in seconds + */ + + async mset( + keys: string[], + values: T[], + ttl = this.defaultTTL, + ): Promise { + await Promise.allSettled( + this.levels.map((level) => + handleGracefully( + () => level.mset(keys, values, ttl), + "Failed to mset keys in cache level", + ), + ), + ); + } + /** * Loop through cache levels to get the values for the given keys, * upon failure backfill missing keys From 9a0404b8b9b2ee8b55ddb0d69df44909c7896c0f Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:58:11 +1100 Subject: [PATCH 5/6] refactor: improve default TTL and Lock TTL assignment logic in CacheService constructor --- src/cache.service.spec.ts | 16 ++++++++-------- src/cache.service.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cache.service.spec.ts b/src/cache.service.spec.ts index 5b2ce88..502c904 100644 --- a/src/cache.service.spec.ts +++ b/src/cache.service.spec.ts @@ -593,15 +593,15 @@ describe("Multi-get, Multi-delete and multi set functionality", () => { const retrievedValues = await cache.mget(keys); expect(retrievedValues).toEqual(values); - + await cache.mdel(keys); expect(await cache.mget(keys)).toEqual([null, null, null]); }); }); -describe('CacheService should instantiate with default TTL and Lock TTL values', () => { - it('should use provided defaultTTL and defaultLockTTL values', () => { +describe("CacheService should instantiate with default TTL and Lock TTL values", () => { + it("should use provided defaultTTL and defaultLockTTL values", () => { const customDefaultTTL = 5000; const customDefaultLockTTL = 100; @@ -611,18 +611,18 @@ describe('CacheService should instantiate with default TTL and Lock TTL values', defaultLockTTL: customDefaultLockTTL, }); - expect(cache['defaultTTL']).toBe(customDefaultTTL); - expect(cache['defaultLockTTL']).toBe(customDefaultLockTTL); + expect(cache.defaultTTL).toBe(customDefaultTTL); + expect(cache.defaultLockTTL).toBe(customDefaultLockTTL); }); - it('should fallback to DEFAULT_TTL and DEFAULT_LOCK_TTL if invalid values are provided', () => { + it("should fallback to DEFAULT_TTL and DEFAULT_LOCK_TTL if invalid values are provided", () => { const cache = new CacheService({ levels: [memoryLevel, redisLevel], defaultTTL: NaN, defaultLockTTL: NaN, }); - expect(cache['defaultTTL']).toBe(3600); // DEFAULT_TTL - expect(cache['defaultLockTTL']).toBe(30); // DEFAULT_LOCK_TTL + expect(cache.defaultTTL).toBe(3600); // DEFAULT_TTL + expect(cache.defaultLockTTL).toBe(30); // DEFAULT_LOCK_TTL }); }); diff --git a/src/cache.service.ts b/src/cache.service.ts index 786f456..ffc5ff0 100644 --- a/src/cache.service.ts +++ b/src/cache.service.ts @@ -26,10 +26,14 @@ export class CacheService { const { defaultTTL, defaultLockTTL, levels, versioning } = options; this.levels = levels; - this.defaultTTL = (typeof defaultTTL === "number" && - !isNaN(defaultTTL)) ? defaultTTL : DEFAULT_TTL; - this.defaultLockTTL = (typeof defaultLockTTL === "number" && - !isNaN(defaultLockTTL)) ? defaultLockTTL : DEFAULT_LOCK_TTL; + this.defaultTTL = + typeof defaultTTL === "number" && !Number.isNaN(defaultTTL) + ? defaultTTL + : DEFAULT_TTL; + this.defaultLockTTL = + typeof defaultLockTTL === "number" && !Number.isNaN(defaultLockTTL) + ? defaultLockTTL + : DEFAULT_LOCK_TTL; this.versioning = versioning ?? false; } From dacad255780afa4df641cf4c7cf3efba3f8c6fba Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 4 Nov 2025 08:58:49 +1100 Subject: [PATCH 6/6] refactor: remove debug log from backfillLevels function --- src/utils/backfill.utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/backfill.utils.ts b/src/utils/backfill.utils.ts index ad95de5..dc7751e 100644 --- a/src/utils/backfill.utils.ts +++ b/src/utils/backfill.utils.ts @@ -50,7 +50,6 @@ export async function backfillLevels( value: unknown, ttl?: number, ): Promise { - console.log("backfillLevels called with key:", key); if (failedLevels.length === 0) return; await Promise.allSettled( failedLevels.map((failedLevel) => {