Scrapboard.org client
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);