Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(packages/cache): Support multi operations #2653

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cache/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
54 changes: 54 additions & 0 deletions packages/cache/src/examples/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]" },
userId2: { id: "userId2", email: "[email protected]" },
});

const usersSwr = await cache.user.swrMany(
["userId", "userId2"],
async (something) => {
const users: Record<string, User> = {};

for (const userId of something) {
users[userId] = {
email: "[email protected]",
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: "[email protected]",
id: userId.toString(),
},
};
});

if (randomUsers.val) {
const { userId, userId2 } = randomUsers.val;
console.info({ userId, userId2 });
}
}

main();
11 changes: 11 additions & 0 deletions packages/cache/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ interface CacheNamespace<TValue> {
key: string,
refreshFromOrigin: (key: string) => Promise<TValue | undefined>,
): Promise<Result<TValue | undefined, CacheError>>;

setMany: (entries: Record<string, TValue>) => Promise<Result<void, CacheError>>;

getMany: <K extends string>(
keys: K[],
) => Promise<Result<Record<K, TValue | undefined>, CacheError>>;

swrMany: <K extends string>(
keys: K[],
refreshFromOrigin: (keys: K[]) => Promise<Record<K, TValue | undefined>>,
) => Promise<Result<Record<K, TValue | undefined>, CacheError>>;
}

export type Cache<TNamespaces extends Record<string, unknown>> = {
Expand Down
29 changes: 26 additions & 3 deletions packages/cache/src/middleware/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export class EncryptedStore<TNamespace extends string, TValue = any>
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);
Expand Down Expand Up @@ -92,6 +95,20 @@ export class EncryptedStore<TNamespace extends string, TValue = any>
return res;
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
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(
Expand Down Expand Up @@ -120,7 +137,9 @@ export class EncryptedStore<TNamespace extends string, TValue = any>

static async fromBase64Key<TNamespace extends string, TValue>(
base64EncodedKey: string,
): Promise<{ wrap: (store: Store<TNamespace, TValue>) => Store<TNamespace, TValue> }> {
): Promise<{
wrap: (store: Store<TNamespace, TValue>) => Store<TNamespace, TValue>;
}> {
const cryptoKey = await crypto.subtle.importKey(
"raw",
decode(base64EncodedKey),
Expand All @@ -133,7 +152,11 @@ export class EncryptedStore<TNamespace extends string, TValue = any>

return {
wrap: (store) =>
new EncryptedStore({ store, encryptionKey: cryptoKey, encryptionKeyHash: hash }),
new EncryptedStore({
store,
encryptionKey: cryptoKey,
encryptionKeyHash: hash,
}),
};
}
}
Expand Down
19 changes: 15 additions & 4 deletions packages/cache/src/middleware/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ class StoreWithMetrics<TNamespace extends string, TValue> implements Store<TName

private readonly metrics: Metrics;

constructor(opts: {
store: Store<TNamespace, TValue>;
metrics: Metrics;
}) {
constructor(opts: { store: Store<TNamespace, TValue>; metrics: Metrics }) {
this.name = opts.store.name;
this.store = opts.store;
this.metrics = opts.metrics;
Expand Down Expand Up @@ -99,6 +96,20 @@ class StoreWithMetrics<TNamespace extends string, TValue> implements Store<TName
return res;
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
return Ok();
}

public async remove(namespace: TNamespace, key: string): Promise<Result<void, CacheError>> {
const start = performance.now();
const res = this.store.remove(namespace, key);
Expand Down
16 changes: 16 additions & 0 deletions packages/cache/src/stores/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ export class CloudflareStore<TNamespace extends string, TValue = any>
}),
);
}

if (!res) {
return Ok(undefined);
}

const raw = await res.text();
try {
const entry = superjson.parse(raw) as Entry<TValue>;
Expand Down Expand Up @@ -139,4 +141,18 @@ export class CloudflareStore<TNamespace extends string, TValue = any>
),
);
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
return Ok();
}
}
21 changes: 21 additions & 0 deletions packages/cache/src/stores/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,25 @@ export interface Store<TNamespace extends string, TValue> {
* Removes the key from the store.
*/
remove(namespace: TNamespace, key: string | string[]): Promise<Result<void, CacheError>>;

/**
* 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<string, Entry<TValue>>,
): Promise<Result<void, CacheError>>;

/**
* Gets the multiple values from the store.
*
* The response must be `undefined` for cache misses
*/
getMany<K extends string>(
namespace: TNamespace,
keys: K[],
): Promise<Result<Record<K, Entry<TValue> | undefined>, CacheError>>;
}
14 changes: 14 additions & 0 deletions packages/cache/src/stores/libsql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,18 @@ export class LibSQLStore<TNamespace extends string, TValue = any>
return Ok();
}
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
return Ok();
}
}
14 changes: 14 additions & 0 deletions packages/cache/src/stores/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,18 @@ export class MemoryStore<TNamespace extends string, TValue = any>
}
return Promise.resolve(Ok());
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
return Ok();
}
}
14 changes: 14 additions & 0 deletions packages/cache/src/stores/upstash-redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,18 @@ export class UpstashRedisStore<TNamespace extends string, TValue = any>
this.redis.del(...cacheKeys);
return Ok();
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok(Object.fromEntries([]));
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
return Ok();
}
}
22 changes: 22 additions & 0 deletions packages/cache/src/swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ export class SwrCache<TNamespace extends string, TValue> {
}
}

public async swrMany<K extends string>(
namespace: TNamespace,
keys: K[],
loadFromOrigin: (keys: K[]) => Promise<Record<string, TValue | undefined>>,
): Promise<Result<Record<K, TValue | undefined>, CacheError>> {
return Ok();
}

public async getMany(
namespace: TNamespace,
keys: string[],
): Promise<Result<Record<string, TValue | undefined>, CacheError>> {
return Ok();
}

public async setMany(
namespace: TNamespace,
entries: Record<string, TValue>,
): Promise<Result<void, CacheError>> {
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.
Expand Down
24 changes: 24 additions & 0 deletions packages/cache/src/tiered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,28 @@ export class TieredStore<TNamespace extends string, TValue> implements Store<TNa
),
);
}

public async setMany(
namespace: TNamespace,
entries: Record<string, Entry<TValue>>,
): Promise<Result<void, CacheError>> {
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<Result<Record<string, Entry<TValue> | undefined>, CacheError>> {
return Ok();
}
}