diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 3f851487cd..ac19f881a0 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -16,6 +16,9 @@ export function createCache< set: (key, value, opts) => c.set(n, key, value, opts), remove: (key) => c.remove(n, key), swr: (key, loadFromOrigin) => c.swr(n, key, loadFromOrigin), + setMany: (entries) => c.setMany(n, entries), + getMany: (keys) => c.getMany(n, keys), + swrMany: (keys, loadFromOrigin) => c.swrMany(n, keys, loadFromOrigin), }; return acc; }, diff --git a/packages/cache/src/examples/memory.ts b/packages/cache/src/examples/memory.ts index 6b58e2ca25..6ab2860d01 100644 --- a/packages/cache/src/examples/memory.ts +++ b/packages/cache/src/examples/memory.ts @@ -32,6 +32,60 @@ async function main() { const user = await cache.user.get("userId"); console.info(user); + + const users = await cache.user.getMany(["userId", "userId2"]); + + if (users.val) { + const { userId, userId2 } = users.val; + + console.info({ userId, userId2 }); + } + + await cache.user.setMany({ + userId1: { id: "userId1", email: "user@email.com" }, + userId2: { id: "userId2", email: "user@email.com" }, + }); + + const usersSwr = await cache.user.swrMany( + ["userId", "userId2"], + async (something) => { + const users: Record = {}; + + for (const userId of something) { + users[userId] = { + email: "user@email.com", + id: userId, + }; + } + + return users; + } + ); + + if (usersSwr.val) { + const { userId, userId2 } = usersSwr.val; + + console.info({ userId, userId2 }); + } + + // generate 4 random numbers + const userIds = Array.from({ length: 4 }, () => + Math.floor(Math.random() * 100).toString() + ); + + const randomUsers = await cache.user.swrMany(userIds, async (userId) => { + return { + userId: { + email: "user@email.com", + id: userId.toString(), + }, + }; + }); + + if (randomUsers.val) { + const { userId, userId2 } = randomUsers.val; + console.info({ userId, userId2 }); + } } main(); diff --git a/packages/cache/src/interface.ts b/packages/cache/src/interface.ts index 28397c88e4..0cc576c44d 100644 --- a/packages/cache/src/interface.ts +++ b/packages/cache/src/interface.ts @@ -34,6 +34,17 @@ interface CacheNamespace { key: string, refreshFromOrigin: (key: string) => Promise, ): Promise>; + + setMany: (entries: Record) => Promise>; + + getMany: ( + keys: K[], + ) => Promise, CacheError>>; + + swrMany: ( + keys: K[], + refreshFromOrigin: (keys: K[]) => Promise>, + ) => Promise, CacheError>>; } export type Cache> = { diff --git a/packages/cache/src/middleware/encryption.ts b/packages/cache/src/middleware/encryption.ts index 56ffa6da37..94bc6091b9 100644 --- a/packages/cache/src/middleware/encryption.ts +++ b/packages/cache/src/middleware/encryption.ts @@ -61,7 +61,10 @@ export class EncryptedStore return res; } try { - const { iv, ciphertext } = res.val.value as { iv: string; ciphertext: string }; + const { iv, ciphertext } = res.val.value as { + iv: string; + ciphertext: string; + }; const decrypted = await this.decrypt(iv, ciphertext); res.val.value = SuperJSON.parse(decrypted); @@ -92,6 +95,20 @@ export class EncryptedStore return res; } + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } + private async encrypt(secret: string): Promise<{ iv: string; ciphertext: string }> { const iv = crypto.getRandomValues(new Uint8Array(32)); const ciphertext = await crypto.subtle.encrypt( @@ -120,7 +137,9 @@ export class EncryptedStore static async fromBase64Key( base64EncodedKey: string, - ): Promise<{ wrap: (store: Store) => Store }> { + ): Promise<{ + wrap: (store: Store) => Store; + }> { const cryptoKey = await crypto.subtle.importKey( "raw", decode(base64EncodedKey), @@ -133,7 +152,11 @@ export class EncryptedStore return { wrap: (store) => - new EncryptedStore({ store, encryptionKey: cryptoKey, encryptionKeyHash: hash }), + new EncryptedStore({ + store, + encryptionKey: cryptoKey, + encryptionKeyHash: hash, + }), }; } } diff --git a/packages/cache/src/middleware/metrics.ts b/packages/cache/src/middleware/metrics.ts index f3789a9d3b..d07c9c85bf 100644 --- a/packages/cache/src/middleware/metrics.ts +++ b/packages/cache/src/middleware/metrics.ts @@ -44,10 +44,7 @@ class StoreWithMetrics implements Store; - metrics: Metrics; - }) { + constructor(opts: { store: Store; metrics: Metrics }) { this.name = opts.store.name; this.store = opts.store; this.metrics = opts.metrics; @@ -99,6 +96,20 @@ class StoreWithMetrics implements Store | undefined>, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } + public async remove(namespace: TNamespace, key: string): Promise> { const start = performance.now(); const res = this.store.remove(namespace, key); diff --git a/packages/cache/src/stores/cloudflare.ts b/packages/cache/src/stores/cloudflare.ts index dcc46383c3..2860977ac7 100644 --- a/packages/cache/src/stores/cloudflare.ts +++ b/packages/cache/src/stores/cloudflare.ts @@ -58,9 +58,11 @@ export class CloudflareStore }), ); } + if (!res) { return Ok(undefined); } + const raw = await res.text(); try { const entry = superjson.parse(raw) as Entry; @@ -139,4 +141,18 @@ export class CloudflareStore ), ); } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } } diff --git a/packages/cache/src/stores/interface.ts b/packages/cache/src/stores/interface.ts index 0b3c1dab92..0c76b56824 100644 --- a/packages/cache/src/stores/interface.ts +++ b/packages/cache/src/stores/interface.ts @@ -50,4 +50,25 @@ export interface Store { * Removes the key from the store. */ remove(namespace: TNamespace, key: string | string[]): Promise>; + + /** + * Sets the multiple values in the store. + * + * You are responsible for evicting expired values in your store implementation. + * Use the `entry.staleUntil` (unix milli timestamp) field to configure expiration + */ + setMany( + namespace: TNamespace, + entries: Record>, + ): Promise>; + + /** + * Gets the multiple values from the store. + * + * The response must be `undefined` for cache misses + */ + getMany( + namespace: TNamespace, + keys: K[], + ): Promise | undefined>, CacheError>>; } diff --git a/packages/cache/src/stores/libsql.ts b/packages/cache/src/stores/libsql.ts index 687593178b..b5829b6719 100644 --- a/packages/cache/src/stores/libsql.ts +++ b/packages/cache/src/stores/libsql.ts @@ -98,4 +98,18 @@ export class LibSQLStore return Ok(); } } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } } diff --git a/packages/cache/src/stores/memory.ts b/packages/cache/src/stores/memory.ts index 8d31bc8537..4b75e18800 100644 --- a/packages/cache/src/stores/memory.ts +++ b/packages/cache/src/stores/memory.ts @@ -108,4 +108,18 @@ export class MemoryStore } return Promise.resolve(Ok()); } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } } diff --git a/packages/cache/src/stores/upstash-redis.ts b/packages/cache/src/stores/upstash-redis.ts index 781159fc81..5c8297e0e1 100644 --- a/packages/cache/src/stores/upstash-redis.ts +++ b/packages/cache/src/stores/upstash-redis.ts @@ -54,4 +54,18 @@ export class UpstashRedisStore this.redis.del(...cacheKeys); return Ok(); } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(Object.fromEntries([])); + } + + public async setMany( + namespace: TNamespace, + entries: Record>, + ): Promise> { + return Ok(); + } } diff --git a/packages/cache/src/swr.ts b/packages/cache/src/swr.ts index 78eb500ec4..744e13ffad 100644 --- a/packages/cache/src/swr.ts +++ b/packages/cache/src/swr.ts @@ -135,6 +135,28 @@ export class SwrCache { } } + public async swrMany( + namespace: TNamespace, + keys: K[], + loadFromOrigin: (keys: K[]) => Promise>, + ): Promise, CacheError>> { + return Ok(); + } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise, CacheError>> { + return Ok(); + } + + public async setMany( + namespace: TNamespace, + entries: Record, + ): Promise> { + return Ok(); + } + /** * Deduplicating the origin load helps when the same value is requested many times at once and is * not yet in the cache. If we don't deduplicate, we'd create a lot of unnecessary load on the db. diff --git a/packages/cache/src/tiered.ts b/packages/cache/src/tiered.ts index 83f053c2d4..d62337a273 100644 --- a/packages/cache/src/tiered.ts +++ b/packages/cache/src/tiered.ts @@ -108,4 +108,28 @@ export class TieredStore implements Store>, + ): Promise> { + return Promise.all(this.tiers.map((t) => t.setMany(namespace, entries))) + .then(() => Ok()) + .catch((err) => + Err( + new CacheError({ + key: "unknown", + tier: this.name, + message: (err as Error).message, + }), + ), + ); + } + + public async getMany( + namespace: TNamespace, + keys: string[], + ): Promise | undefined>, CacheError>> { + return Ok(); + } }