an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
1import { atom } from "jotai";
2import { atomWithStorage } from "jotai/utils";
3
4import type { ContentLabel, LabelerDefinition } from "~/types/moderation";
5
6// --- Configuration ---
7export const CACHE_TIMEOUT_MS = 3600000; // 1 Hour
8const MAX_CACHE_ENTRIES = 2000; // Limit to prevent localStorage quota issues
9const STORAGE_KEY = "moderation-cache-v1";
10
11// --- Types ---
12type CacheEntry = { labels: ContentLabel[]; timestamp: number };
13type CacheMap = Map<string, CacheEntry>;
14
15// --- Custom Storage Implementation ---
16// We cannot use createJSONStorage because it fails to serialize Maps.
17// We must write the storage logic manually.
18const mapStorage = {
19 getItem: (key: string, initialValue: CacheMap): CacheMap => {
20 if (typeof window === "undefined" || !window.localStorage) {
21 return initialValue;
22 }
23
24 try {
25 const item = localStorage.getItem(key);
26 if (!item) return initialValue;
27
28 const parsed = JSON.parse(item);
29
30 // Ensure it is an array (Map serialization format)
31 if (!Array.isArray(parsed)) return initialValue;
32
33 const now = Date.now();
34 const map = new Map<string, CacheEntry>();
35
36 parsed.forEach(([uri, data]) => {
37 // 1. STALENESS CHECK (On Load)
38 // Only load if younger than timeout
39 if (data && now - data.timestamp < CACHE_TIMEOUT_MS) {
40 map.set(uri, data);
41 }
42 });
43
44 console.log(`[Cache] Hydrated ${map.size} valid entries.`);
45 return map;
46 } catch (error) {
47 console.error("[Cache] Failed to load:", error);
48 return initialValue;
49 }
50 },
51
52 setItem: (key: string, value: CacheMap) => {
53 if (typeof window === "undefined" || !window.localStorage) return;
54
55 try {
56 let entries = Array.from(value.entries());
57
58 // 2. SAFETY CAP (On Save)
59 // If we have too many entries, keep only the newest ones
60 if (entries.length > MAX_CACHE_ENTRIES) {
61 // Sort by timestamp descending (newest first)
62 entries.sort((a, b) => b[1].timestamp - a[1].timestamp);
63 // Keep top N
64 entries = entries.slice(0, MAX_CACHE_ENTRIES);
65 }
66
67 // Convert Map -> Array -> JSON String
68 localStorage.setItem(key, JSON.stringify(entries));
69 } catch (error) {
70 console.error("[Cache] Failed to save:", error);
71 }
72 },
73
74 removeItem: (key: string) => {
75 if (typeof window !== "undefined" && window.localStorage) {
76 localStorage.removeItem(key);
77 }
78 },
79};
80
81// --- Atoms ---
82
83export const labelerConfigAtom = atom<LabelerDefinition[]>([]);
84
85export const moderationCacheAtom = atomWithStorage<CacheMap>(
86 STORAGE_KEY,
87 new Map(),
88 mapStorage // <--- Pass our custom object here
89);
90
91export const pendingUriQueueAtom = atom<Set<string>>(new Set<string>());
92export const processingUriSetAtom = atom<Set<string>>(new Set<string>());