From 4f1298609d1375c11c1518252543ffbb7ee59d30 Mon Sep 17 00:00:00 2001 From: Flo <53355483+Flo4604@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:49:10 +0100 Subject: [PATCH 1/3] feat(packages/cache): Add inital overview on how the interface for mult operations could look like --- packages/cache/src/cache.ts | 3 ++ packages/cache/src/examples/memory.ts | 34 +++++++++++++++++++++ packages/cache/src/interface.ts | 11 +++++++ packages/cache/src/middleware/encryption.ts | 29 ++++++++++++++++-- packages/cache/src/middleware/metrics.ts | 19 +++++++++--- packages/cache/src/stores/cloudflare.ts | 16 ++++++++++ packages/cache/src/stores/interface.ts | 21 +++++++++++++ packages/cache/src/stores/libsql.ts | 14 +++++++++ packages/cache/src/stores/memory.ts | 14 +++++++++ packages/cache/src/stores/upstash-redis.ts | 14 +++++++++ packages/cache/src/swr.ts | 22 +++++++++++++ packages/cache/src/tiered.ts | 24 +++++++++++++++ 12 files changed, 214 insertions(+), 7 deletions(-) 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..67c4c999bd 100644 --- a/packages/cache/src/examples/memory.ts +++ b/packages/cache/src/examples/memory.ts @@ -32,6 +32,40 @@ 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" }, + }); + + if (users.val) { + const { userId, userId2 } = users.val; + + console.info({ userId, userId2 }); + } + + const usersSwr = await cache.user.swrMany(["userId", "userId2"], async (_) => { + return { + userId: { + email: "user@email.com", + id: "userId", + }, + }; + }); + + if (usersSwr.val) { + const { userId, userId2 } = usersSwr.val; + + console.info({ userId, userId2 }); + } } main(); diff --git a/packages/cache/src/interface.ts b/packages/cache/src/interface.ts index 28397c88e4..55b409343c 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(); + } } From eb5b256832199450dd158ff663762ba75771d4b8 Mon Sep 17 00:00:00 2001 From: Flo <53355483+Flo4604@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:24:06 +0100 Subject: [PATCH 2/3] chore: update example --- packages/cache/src/examples/memory.ts | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/cache/src/examples/memory.ts b/packages/cache/src/examples/memory.ts index 67c4c999bd..f081c242f9 100644 --- a/packages/cache/src/examples/memory.ts +++ b/packages/cache/src/examples/memory.ts @@ -46,24 +46,40 @@ async function main() { userId2: { id: "userId2", email: "user@email.com" }, }); - if (users.val) { - const { userId, userId2 } = users.val; + const usersSwr = await cache.user.swrMany( + ["userId", "userId2"], + async (_) => { + return { + userId: { + email: "user@email.com", + id: "userId", + }, + }; + } + ); + + if (usersSwr.val) { + const { userId, userId2 } = usersSwr.val; console.info({ userId, userId2 }); } - const usersSwr = await cache.user.swrMany(["userId", "userId2"], async (_) => { + // 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", + id: userId.toString(), }, }; }); - if (usersSwr.val) { - const { userId, userId2 } = usersSwr.val; - + if (randomUsers.val) { + const { userId, userId2 } = randomUsers.val; console.info({ userId, userId2 }); } } From a5b22a86cf4f8e62dc2ef17f960d50ff47e3e6f1 Mon Sep 17 00:00:00 2001 From: Flo <53355483+Flo4604@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:31:33 +0100 Subject: [PATCH 3/3] fix: adjust interface to type keys --- packages/cache/src/examples/memory.ts | 16 ++++++++++------ packages/cache/src/interface.ts | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/cache/src/examples/memory.ts b/packages/cache/src/examples/memory.ts index f081c242f9..6ab2860d01 100644 --- a/packages/cache/src/examples/memory.ts +++ b/packages/cache/src/examples/memory.ts @@ -48,13 +48,17 @@ async function main() { const usersSwr = await cache.user.swrMany( ["userId", "userId2"], - async (_) => { - return { - userId: { + async (something) => { + const users: Record = {}; + + for (const userId of something) { + users[userId] = { email: "user@email.com", - id: "userId", - }, - }; + id: userId, + }; + } + + return users; } ); diff --git a/packages/cache/src/interface.ts b/packages/cache/src/interface.ts index 55b409343c..0cc576c44d 100644 --- a/packages/cache/src/interface.ts +++ b/packages/cache/src/interface.ts @@ -43,7 +43,7 @@ interface CacheNamespace { swrMany: ( keys: K[], - refreshFromOrigin: (keys: K[]) => Promise>, + refreshFromOrigin: (keys: K[]) => Promise>, ) => Promise, CacheError>>; }