refactor: simplify Phase 1 infrastructure code

- cache-invalidation: extract isExpectedRevalidateError helper
- integration-data-cache: flatten early-return chain, extract eviction
- redis-handler: extract StoredEntry type, readStreamBody, ensureConnected
- svg-purify: single getOwnPropertyDescriptor lookup per prop
- session-scoped-providers: simplify auth flow
- retryable-loader: use function declarations
- translation/request: replace spread-in-reduce with for loop
This commit is contained in:
Thomas Camlong committed 2026-08-07 21:04:42 +02:00
1 parent b3b2d5a022
commit 33293ba1cf
8 files changed
+148 -134

No files matched your search

@@ -4,8 +4,55 @@ import type { CacheHandler } from "next/dist/server/lib/cache-handlers/types";
type CacheEntry = Awaited<ReturnType<CacheHandler["get"]>> & {};
type StoredEntry = {
tags: string[];
stale: number;
timestamp: number;
expire: number;
revalidate: number;
body: number[];
};
// eslint-disable-next-line no-underscore-dangle
const BUILD_ID = process.env.__NEXT_BUILD_ID ?? "dev";
const TAG_TTL_MS = 7 * 24 * 60 * 60 * 1000;
async function readStreamBody(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
let done = false;
while (!done) {
const result = await reader.read();
done = result.done;
if (result.value) chunks.push(result.value);
}
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
const body = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.length;
}
return body;
}
function entryFromStored(parsed: StoredEntry): CacheEntry {
const body = new Uint8Array(parsed.body);
return {
value: new ReadableStream({
start(controller) {
controller.enqueue(body);
controller.close();
},
}),
tags: parsed.tags,
stale: parsed.stale,
timestamp: parsed.timestamp,
expire: parsed.expire,
revalidate: parsed.revalidate,
};
}
export class RedisCacheHandler implements CacheHandler {
private redis: Redis;
@@ -38,49 +85,32 @@ export class RedisCacheHandler implements CacheHandler {
return `nextCache:tag:${tag}`;
}
private async ensureConnected() {
await this.redis.connect().catch(() => {});
}
async get(cacheKey: string, softTags: string[]): Promise<CacheEntry | undefined> {
const pending = this.pendingSets.get(cacheKey);
if (pending) return pending;
try {
await this.redis.connect().catch(() => {});
await this.ensureConnected();
const raw = await this.redis.getBuffer(this.entryKey(cacheKey));
if (!raw) return undefined;
const parsed = JSON.parse(raw.toString()) as {
tags: string[];
stale: number;
timestamp: number;
expire: number;
revalidate: number;
body: number[];
};
const parsed = JSON.parse(raw.toString()) as StoredEntry;
if (parsed.expire <= 0) {
await this.redis.del(this.entryKey(cacheKey)).catch(() => {});
return undefined;
}
const allTags = [...parsed.tags, ...softTags];
const expiration = await this.getExpiration(allTags);
const expiration = await this.getExpiration([...parsed.tags, ...softTags]);
if (expiration !== 0 && expiration > parsed.timestamp) {
return undefined;
}
const body = new Uint8Array(parsed.body);
return {
value: new ReadableStream({
start(controller) {
controller.enqueue(body);
controller.close();
},
}),
tags: parsed.tags,
stale: parsed.stale,
timestamp: parsed.timestamp,
expire: parsed.expire,
revalidate: parsed.revalidate,
};
return entryFromStored(parsed);
} catch {
return undefined;
}
@@ -91,22 +121,10 @@ export class RedisCacheHandler implements CacheHandler {
try {
const entry = await pendingEntry;
const reader = entry.value.getReader();
const chunks: Uint8Array[] = [];
let done = false;
while (!done) {
const result = await reader.read();
done = result.done;
if (result.value) chunks.push(result.value);
}
const body = await readStreamBody(entry.value);
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
const body = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.length;
}
const ttlMs = entry.expire * 1000;
if (ttlMs <= 0) return; // Dynamic entries (expire: 0) should not be persisted
const serialized = JSON.stringify({
tags: entry.tags,
@@ -117,9 +135,7 @@ export class RedisCacheHandler implements CacheHandler {
body: Array.from(body),
});
const ttlMs = entry.expire * 1000;
if (ttlMs <= 0) return; // Dynamic entries (expire: 0) should not be persisted
await this.redis.connect().catch(() => {});
await this.ensureConnected();
await this.redis.set(this.entryKey(cacheKey), serialized, "PX", ttlMs);
} catch {
// Fail open
@@ -132,20 +148,19 @@ export class RedisCacheHandler implements CacheHandler {
async refreshTags(): Promise<void> {
try {
await this.redis.connect().catch(() => {});
await this.ensureConnected();
this.tagCache.clear();
let cursor = "0";
do {
const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", "nextCache:tag:*", "COUNT", 100);
cursor = nextCursor;
if (keys.length > 0) {
const values = await this.redis.mget(...keys);
for (let i = 0; i < keys.length; i++) {
const tag = keys[i]?.replace("nextCache:tag:", "");
if (!tag) continue;
const val = values[i];
if (val) this.tagCache.set(tag, Number(val));
}
if (keys.length === 0) continue;
const values = await this.redis.mget(...keys);
for (let i = 0; i < keys.length; i++) {
const tag = keys[i]?.replace("nextCache:tag:", "");
const val = values[i];
if (tag && val) this.tagCache.set(tag, Number(val));
}
} while (cursor !== "0");
} catch {
@@ -170,17 +185,15 @@ export class RedisCacheHandler implements CacheHandler {
if (missingTags.length > 0) {
try {
await this.redis.connect().catch(() => {});
const keys = missingTags.map((t) => this.tagKey(t));
const values = await this.redis.mget(...keys);
await this.ensureConnected();
const values = await this.redis.mget(...missingTags.map((tag) => this.tagKey(tag)));
for (let i = 0; i < missingTags.length; i++) {
const val = values[i];
if (val) {
const ts = Number(val);
const missingTag = missingTags[i];
if (missingTag) this.tagCache.set(missingTag, ts);
if (ts > maxTimestamp) maxTimestamp = ts;
}
const tag = missingTags[i];
if (!val || !tag) continue;
const timestamp = Number(val);
this.tagCache.set(tag, timestamp);
if (timestamp > maxTimestamp) maxTimestamp = timestamp;
}
} catch {
// Fail open
@@ -193,17 +206,14 @@ export class RedisCacheHandler implements CacheHandler {
async updateTags(tags: string[], durations?: { expire?: number }): Promise<void> {
if (tags.length === 0) return;
try {
await this.redis.connect().catch(() => {});
await this.ensureConnected();
const now = Date.now();
const expireMs = durations?.expire ? durations.expire * 1000 : TAG_TTL_MS;
const pipeline = this.redis.pipeline();
for (const tag of tags) {
const key = this.tagKey(tag);
pipeline.set(key, String(now));
if (durations?.expire) {
pipeline.pexpire(key, durations.expire * 1000);
} else {
pipeline.pexpire(key, 7 * 24 * 60 * 60 * 1000);
}
pipeline.pexpire(key, expireMs);
this.tagCache.set(tag, now);
}
await pipeline.exec();