release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+151
View File
@@ -0,0 +1,151 @@
export interface CacheEntry<V> {
value: V;
createdAt: number;
expiresAt: number;
staleUntil: number;
tags: string[];
}
export type CacheLookup<V> = { state: "miss" } | { state: "fresh" | "stale"; entry: CacheEntry<V> };
export interface CacheSetOptions {
ttlMs?: number;
staleWhileRevalidateMs?: number;
tags?: string[];
}
export interface TagCacheOptions {
ttlMs?: number;
staleWhileRevalidateMs?: number;
maxEntries?: number;
clock?: () => number;
}
export class TagCache<V = unknown> {
private entries = new Map<string, CacheEntry<V>>();
private tagIndex = new Map<string, Set<string>>();
private pending = new Map<string, Promise<V>>();
private readonly ttlMs: number;
private readonly staleMs: number;
private readonly maxEntries: number;
private readonly clock: () => number;
constructor(options: TagCacheOptions = {}) {
this.ttlMs = options.ttlMs ?? 60_000;
this.staleMs = options.staleWhileRevalidateMs ?? 0;
this.maxEntries = Math.max(1, options.maxEntries ?? 10_000);
this.clock = options.clock ?? Date.now;
}
lookup(key: string): CacheLookup<V> {
const entry = this.entries.get(key);
if (!entry) return { state: "miss" };
const now = this.clock();
if (entry.staleUntil <= now) {
this.delete(key);
return { state: "miss" };
}
this.entries.delete(key);
this.entries.set(key, entry);
return { state: entry.expiresAt > now ? "fresh" : "stale", entry };
}
get(key: string): V | undefined {
const hit = this.lookup(key);
return hit.state === "miss" ? undefined : hit.entry.value;
}
set(key: string, value: V, options: CacheSetOptions = {}): void {
this.delete(key);
const now = this.clock();
const ttlMs = Math.max(0, options.ttlMs ?? this.ttlMs);
const staleMs = Math.max(0, options.staleWhileRevalidateMs ?? this.staleMs);
const tags = [...new Set(options.tags ?? [])];
const entry: CacheEntry<V> = {
value,
createdAt: now,
expiresAt: now + ttlMs,
staleUntil: now + ttlMs + staleMs,
tags,
};
this.entries.set(key, entry);
for (const tag of tags) {
const keys = this.tagIndex.get(tag) ?? new Set<string>();
keys.add(key);
this.tagIndex.set(tag, keys);
}
while (this.entries.size > this.maxEntries) {
const oldest = this.entries.keys().next().value as string | undefined;
if (oldest === undefined) break;
this.delete(oldest);
}
}
async getOrLoad(
key: string,
loader: () => V | Promise<V>,
options: CacheSetOptions = {},
): Promise<V> {
const hit = this.lookup(key);
if (hit.state === "fresh") return hit.entry.value;
if (hit.state === "stale") {
if (!this.pending.has(key)) {
const refresh = Promise.resolve()
.then(loader)
.then((value) => {
this.set(key, value, options);
return value;
})
.finally(() => this.pending.delete(key));
this.pending.set(key, refresh);
}
return hit.entry.value;
}
const existing = this.pending.get(key);
if (existing) return existing;
const pending = Promise.resolve()
.then(loader)
.then((value) => {
this.set(key, value, options);
return value;
})
.finally(() => this.pending.delete(key));
this.pending.set(key, pending);
return pending;
}
delete(key: string): boolean {
const entry = this.entries.get(key);
if (!entry) return false;
this.entries.delete(key);
for (const tag of entry.tags) {
const keys = this.tagIndex.get(tag);
keys?.delete(key);
if (keys?.size === 0) this.tagIndex.delete(tag);
}
return true;
}
invalidateTag(tag: string): number {
const keys = [...(this.tagIndex.get(tag) ?? [])];
for (const key of keys) this.delete(key);
return keys.length;
}
invalidateTags(tags: Iterable<string>): number {
const keys = new Set<string>();
for (const tag of tags) for (const key of this.tagIndex.get(tag) ?? []) keys.add(key);
for (const key of keys) this.delete(key);
return keys.size;
}
clear(): void {
this.entries.clear();
this.tagIndex.clear();
this.pending.clear();
}
get size(): number {
return this.entries.size;
}
}