Scrapboard.org client
at main 129 lines 3.2 kB view raw
1import { create } from "zustand"; 2import { persist } from "zustand/middleware"; 3import { DidDocument } from "@atcute/identity"; 4import { createMapStorage } from "../utils/mapStorage"; 5 6// Cache policy 7const STALE_AFTER = 5 * 60 * 1000; // 5 minutes 8const EXPIRE_AFTER = 60 * 60 * 1000; // 1 hour 9 10export type CacheResult = { 11 did: string; 12 doc: DidDocument; 13 updatedAt: number; 14 stale: boolean; 15 expired: boolean; 16}; 17 18export type DidCache = { 19 dids: Map<string, CacheResult>; 20 setDid: (did: string, doc: DidDocument) => CacheResult; 21 getDid: (did: string) => CacheResult | undefined; 22 checkCache: (did: string) => CacheResult | null; 23 refreshCache: ( 24 did: string, 25 fetchFn: () => Promise<DidDocument | null>, 26 prev?: CacheResult 27 ) => Promise<void>; 28 clearEntry: (did: string) => void; 29 clear: () => void; 30}; 31 32export const useDidStore = create<DidCache>()( 33 persist( 34 (set, get) => ({ 35 dids: new Map(), 36 37 setDid: (did, doc) => { 38 const now = Date.now(); 39 const entry: CacheResult = { 40 did, 41 doc, 42 updatedAt: now, 43 stale: false, 44 expired: false, 45 }; 46 set((state) => ({ 47 dids: new Map(state.dids).set(did, entry), 48 })); 49 return entry; 50 }, 51 52 getDid: (did) => { 53 return get().dids.get(did); 54 }, 55 56 checkCache: (did) => { 57 const entry = get().dids.get(did); 58 if (!entry) return null; 59 60 const now = Date.now(); 61 const age = now - entry.updatedAt; 62 const stale = age > STALE_AFTER; 63 const expired = age > EXPIRE_AFTER; 64 65 if (stale !== entry.stale || expired !== entry.expired) { 66 const updated: CacheResult = { 67 ...entry, 68 stale, 69 expired, 70 }; 71 set((state) => ({ 72 dids: new Map(state.dids).set(did, updated), 73 })); 74 return updated; 75 } 76 77 return entry; 78 }, 79 80 refreshCache: async ( 81 did: string, 82 fetchFn: () => Promise<DidDocument | null>, 83 prev?: CacheResult 84 ) => { 85 try { 86 const doc = await fetchFn(); 87 if (doc) { 88 get().setDid(did, doc); 89 } else if (prev) { 90 // keep existing but mark as expired 91 set((state) => { 92 const updated: CacheResult = { 93 ...prev, 94 stale: true, 95 expired: true, 96 }; 97 return { 98 dids: new Map(state.dids).set(did, updated), 99 }; 100 }); 101 } else { 102 get().clearEntry(did); 103 } 104 } catch { 105 // network or validation failure — don't overwrite unless necessary 106 } 107 }, 108 109 clearEntry: (did) => { 110 set((state) => { 111 const map = new Map(state.dids); 112 map.delete(did); 113 return { dids: map }; 114 }); 115 }, 116 117 clear: () => { 118 set(() => ({ dids: new Map() })); 119 }, 120 }), 121 { 122 name: "dids", 123 partialize: (state) => ({ 124 dids: state.dids, 125 }), 126 storage: createMapStorage("dids"), 127 } 128 ) 129);