Skip to content

Commit

Permalink
Make it so that the BUILD_ID is only ever read once in the server lif…
Browse files Browse the repository at this point in the history
…etime (#120)

* ♻️ make it so that the BUILD_ID is only ever read once in the server lifetime

* 💡 more comments on the `lifetime-cache` fn
  • Loading branch information
Fredkiss3 committed Jan 5, 2024
1 parent 9e1d42c commit 42fe696
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 106 deletions.
12 changes: 5 additions & 7 deletions src/components/cache.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { createCacheComponent } from "@rsc-cache/next";
import { cache } from "react";
import fs from "fs/promises";
import { kv } from "~/lib/server/kv/index.server";
import { DEFAULT_CACHE_TTL } from "~/lib/shared/constants";
import { lifetimeCache } from "~/lib/shared/lifetime-cache";

const devBuildId = new Date().getTime().toString();
const getBuildId = cache(async () => {
if (process.env.NODE_ENV === "development") {
return devBuildId;
}
return await fs.readFile(".next/BUILD_ID", "utf-8");
const getBuildId = lifetimeCache(async () => {
return process.env.NODE_ENV === "development"
? new Date().getTime().toString()
: await fs.readFile(".next/BUILD_ID", "utf-8");
});

export const Cache = createCacheComponent({
Expand Down
99 changes: 0 additions & 99 deletions src/lib/shared/fn-cache.ts

This file was deleted.

127 changes: 127 additions & 0 deletions src/lib/shared/lifetime-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Custom `cache` but for caching items for the scope for the lifetime of the server,
* on the client it will cache the result until the tab is refreshed.
*
* It has the same behavior as React `cache`, when called with the same arguments, it will
* return the cached result, if you give it other arguments, it will return a different result
* and cache that.
*
* However this cache is not limitless, it acts as an LRU cache with only a limited set of items in it
* and evicts the least recently used items.
*/
export function lifetimeCache<T extends (...args: any[]) => Promise<any>>(
fn: T
) {
const cache = new PromiseCache<Awaited<ReturnType<T>>>(500);

return function cachedFn(
...args: Parameters<T>
): Thenable<Awaited<ReturnType<T>>> {
return cache.get(args, async () => await fn(...args));
};
}

/**
* LRU Cache utility for usage with fnCache
*/
class PromiseCache<T> {
#cache: Map<
any[],
{
value: Thenable<T>;
fetchFn: () => Promise<T>;
}
>;
#maxSize: number;

constructor(maxSize: number) {
this.#cache = new Map();
this.#maxSize = maxSize;
}

get(args: any[], fetchFn: () => Promise<T>): Thenable<T> {
const existingKey = Array.from(this.#cache.keys()).find((key) => {
return (
args.length === key.length &&
args.every((arg, index) => arg === key[index])
);
});

const cachedItem = existingKey ? this.#cache.get(existingKey) : null;

if (cachedItem) {
return cachedItem.value;
}

/**
* We override the promise result and add `status`
* so that if used with `React.use`,
* the component doesn't suspend if the function is preloaded.
*/
const thenableValue = fetchFn() as Thenable<T>;

thenableValue
.then((value) => {
if (thenableValue.status === "pending") {
const fulfilledThenable =
thenableValue as unknown as FulfilledThenable<T>;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = value;
}
return value;
})
.catch((error) => {
if (thenableValue.status === "pending") {
const rejectedThenable =
thenableValue as unknown as RejectedThenable<T>;
rejectedThenable.status = "rejected";
rejectedThenable.reason = error;
}
throw error;
});
thenableValue.status = "pending";

// Fetch or revalidate the data
this.set(args, thenableValue, fetchFn);
return thenableValue;
}

private set(
key: any[],
valuePromise: Thenable<T>,
fetchFn: () => Promise<T>
): void {
if (this.#cache.size >= this.#maxSize && !this.#cache.has(key)) {
// Evict the least recently used item
const firstKey = this.#cache.keys().next().value;
this.#cache.delete(firstKey);
}

this.#cache.set(key, {
value: valuePromise,
fetchFn
});
}
}

/**
* From the React `use` internals
*/
interface FulfilledThenable<T> extends Promise<T> {
status: "fulfilled";
value: T;
}

interface PendingThenable<T> extends Promise<T> {
status: "pending";
}

interface RejectedThenable<T> extends Promise<T> {
status: "rejected";
reason: any;
}

export type Thenable<T> =
| FulfilledThenable<T>
| PendingThenable<T>
| RejectedThenable<T>;

0 comments on commit 42fe696

Please sign in to comment.