Skip to content

Commit 9fa0e23

Browse files
authored
Merge pull request #417 from docknetwork/DCKA-4922-improve-did-cache
improve DID cache to use individual storage keys
2 parents 9300b55 + 58683c6 commit 9fa0e23

5 files changed

Lines changed: 214 additions & 80 deletions

File tree

packages/react-native/lib/rn-rpc-server/async-storage-service.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export class StorageService {
55
setItem: StorageService.prototype.setItem,
66
getItem: StorageService.prototype.getItem,
77
removeItem: StorageService.prototype.removeItem,
8+
getAllKeys: StorageService.prototype.getAllKeys,
89
};
910

1011
constructor() {
Lines changed: 176 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { CachedDIDResolver } from './cached-did-resolver';
2+
import { storageService } from '../storage';
23

34
const mockStorageService = {
45
getItem: jest.fn(),
56
setItem: jest.fn(),
7+
removeItem: jest.fn(),
8+
getAllKeys: jest.fn(),
69
};
710

811
jest.mock('../storage', () => ({
@@ -20,13 +23,13 @@ describe('CachedDIDResolver', () => {
2023
beforeEach(() => {
2124
jest.clearAllMocks();
2225
mockStorageService.getItem.mockResolvedValue(null);
26+
mockStorageService.getAllKeys.mockResolvedValue([]);
2327
resolver = new CachedDIDResolver(mockRouter);
2428
});
2529

2630
describe('constructor', () => {
2731
it('should initialize with default TTL', () => {
2832
expect(resolver).toBeDefined();
29-
expect(mockStorageService.getItem).toHaveBeenCalledWith('did-cache');
3033
});
3134

3235
it('should initialize with custom TTL', () => {
@@ -42,27 +45,54 @@ describe('CachedDIDResolver', () => {
4245

4346
it('should resolve DID and cache the result on cache miss', async () => {
4447
mockRouter.resolve.mockResolvedValue(testResult);
48+
mockStorageService.getItem.mockResolvedValue(null);
4549

4650
const result = await resolver.resolve(testDid);
4751

4852
expect(mockRouter.resolve).toHaveBeenCalledWith(testDid);
4953
expect(result).toEqual(testResult);
50-
expect(mockStorageService.setItem).toHaveBeenCalled();
54+
expect(mockStorageService.setItem).toHaveBeenCalledWith(
55+
`did-cache:${testDid}`,
56+
JSON.stringify({
57+
value: testResult,
58+
id: testDid,
59+
timestamp: expect.any(Number),
60+
})
61+
);
62+
});
63+
64+
it('should use result.id for cache key instead of input did', async () => {
65+
const inputDid = 'did:example:input';
66+
const resultDid = 'did:example:resolved';
67+
const testResultWithDifferentId = { id: resultDid, publicKey: [] };
68+
69+
mockRouter.resolve.mockResolvedValue(testResultWithDifferentId);
70+
mockStorageService.getItem.mockResolvedValue(null);
71+
72+
const result = await resolver.resolve(inputDid);
73+
74+
expect(result).toEqual(testResultWithDifferentId);
75+
// Should use result.id (resultDid) for the cache key, not inputDid
76+
expect(mockStorageService.setItem).toHaveBeenCalledWith(
77+
`did-cache:${resultDid}`,
78+
JSON.stringify({
79+
value: testResultWithDifferentId,
80+
id: resultDid,
81+
timestamp: expect.any(Number),
82+
})
83+
);
5184
});
5285

5386
it('should return cached result on cache hit', async () => {
5487
const cachedData = {
55-
[testDid]: {
56-
value: testResult,
57-
id: testDid,
58-
timestamp: Date.now(),
59-
},
88+
value: testResult,
89+
id: testDid,
90+
timestamp: Date.now(),
6091
};
61-
92+
6293
mockStorageService.getItem.mockResolvedValue(JSON.stringify(cachedData));
63-
const resolverWithCache = new CachedDIDResolver(mockRouter);
64-
65-
const result = await resolverWithCache.resolve(testDid);
94+
95+
const result = await resolver.resolve(testDid);
6696

6797
expect(result).toEqual(testResult);
6898
expect(mockRouter.resolve).not.toHaveBeenCalled();
@@ -72,72 +102,131 @@ describe('CachedDIDResolver', () => {
72102
const expiredTimestamp = Date.now() - (8 * 24 * 60 * 60 * 1000); // 8 days ago (expired)
73103
const staleResult = { id: testDid, publicKey: [], stale: true };
74104
const freshResult = { id: testDid, publicKey: [], fresh: true };
75-
105+
76106
const cachedData = {
77-
[testDid]: {
78-
value: staleResult,
79-
id: testDid,
80-
timestamp: expiredTimestamp,
81-
},
107+
value: staleResult,
108+
id: testDid,
109+
timestamp: expiredTimestamp,
82110
};
83111

84112
mockStorageService.getItem.mockResolvedValue(JSON.stringify(cachedData));
85113
mockRouter.resolve.mockResolvedValue(freshResult);
86-
87-
const resolverWithExpiredCache = new CachedDIDResolver(mockRouter);
88-
const result = await resolverWithExpiredCache.resolve(testDid);
114+
115+
const result = await resolver.resolve(testDid);
89116

90117
// Should return stale value immediately
91118
expect(result).toEqual(staleResult);
92-
119+
93120
// Background refresh should be triggered (not awaited)
94121
// Give it time to complete
95-
await new Promise(resolve => setTimeout(resolve, 10));
96-
122+
await new Promise(resolve => setTimeout(resolve, 50));
123+
97124
expect(mockRouter.resolve).toHaveBeenCalledWith(testDid);
125+
expect(mockStorageService.setItem).toHaveBeenCalledWith(
126+
`did-cache:${testDid}`,
127+
JSON.stringify({
128+
value: freshResult,
129+
id: testDid,
130+
timestamp: expect.any(Number),
131+
})
132+
);
98133
});
99-
});
100134

101-
describe('getCache', () => {
102-
it('should return the current cache', () => {
103-
const cache = resolver.getCache();
104-
expect(cache).toBeDefined();
105-
expect(typeof cache).toBe('object');
135+
it('should handle corrupted cache data gracefully', async () => {
136+
const testDid = 'did:example:corrupted';
137+
const testResult = { id: testDid, publicKey: [] };
138+
139+
// Return invalid JSON from storage
140+
mockStorageService.getItem.mockResolvedValue('invalid json {');
141+
mockRouter.resolve.mockResolvedValue(testResult);
142+
143+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
144+
145+
const result = await resolver.resolve(testDid);
146+
147+
expect(consoleSpy).toHaveBeenCalledWith('Error parsing cache entry:', expect.any(Error));
148+
expect(mockRouter.resolve).toHaveBeenCalledWith(testDid);
149+
expect(result).toEqual(testResult);
150+
151+
consoleSpy.mockRestore();
106152
});
107153
});
108154

109-
describe('setCache', () => {
110-
it('should set the cache', () => {
111-
const newCache = { 'did:example:456': { value: {}, id: 'did:example:456', timestamp: Date.now() } };
112-
resolver.setCache(newCache);
113-
expect(resolver.getCache()).toEqual(newCache);
155+
describe('getCachedDIDs', () => {
156+
it('should return empty array when no cached DIDs exist', async () => {
157+
mockStorageService.getAllKeys.mockResolvedValue([]);
158+
159+
const cachedDIDs = await resolver.getCachedDIDs();
160+
161+
expect(cachedDIDs).toEqual([]);
162+
expect(mockStorageService.getAllKeys).toHaveBeenCalled();
163+
});
164+
165+
it('should return list of cached DIDs', async () => {
166+
const allKeys = [
167+
'did-cache:did:example:123',
168+
'did-cache:did:example:456',
169+
'did-cache:did:dock:789',
170+
'other-key:something',
171+
'user-preference:theme',
172+
];
173+
174+
mockStorageService.getAllKeys.mockResolvedValue(allKeys);
175+
176+
const cachedDIDs = await resolver.getCachedDIDs();
177+
178+
expect(cachedDIDs).toEqual([
179+
'did:example:123',
180+
'did:example:456',
181+
'did:dock:789',
182+
]);
183+
expect(mockStorageService.getAllKeys).toHaveBeenCalled();
184+
});
185+
186+
it('should handle empty storage correctly', async () => {
187+
mockStorageService.getAllKeys.mockResolvedValue([]);
188+
189+
const cachedDIDs = await resolver.getCachedDIDs();
190+
191+
expect(cachedDIDs).toEqual([]);
114192
});
115193
});
116194

117195
describe('clearCache', () => {
118-
beforeEach(() => {
119-
const cache = {
120-
'did:example:123': { value: {}, id: 'did:example:123', timestamp: Date.now() },
121-
'did:example:456': { value: {}, id: 'did:example:456', timestamp: Date.now() },
122-
};
123-
resolver.setCache(cache);
196+
it('should clear specific DID from cache', async () => {
197+
const didToRemove = 'did:example:123';
198+
199+
await resolver.clearCache(didToRemove);
200+
201+
expect(mockStorageService.removeItem).toHaveBeenCalledWith(`did-cache:${didToRemove}`);
202+
expect(mockStorageService.removeItem).toHaveBeenCalledTimes(1);
124203
});
125204

126-
it('should clear specific DID from cache', () => {
127-
resolver.clearCache('did:example:123');
128-
129-
const cache = resolver.getCache();
130-
expect(cache['did:example:123']).toBeUndefined();
131-
expect(cache['did:example:456']).toBeDefined();
132-
expect(mockStorageService.setItem).toHaveBeenCalled();
205+
it('should clear entire cache when no DID specified', async () => {
206+
const allKeys = [
207+
'did-cache:did:example:123',
208+
'did-cache:did:example:456',
209+
'other-key:something',
210+
];
211+
212+
mockStorageService.getAllKeys.mockResolvedValue(allKeys);
213+
214+
await resolver.clearCache();
215+
216+
expect(mockStorageService.getAllKeys).toHaveBeenCalled();
217+
expect(mockStorageService.removeItem).toHaveBeenCalledWith('did-cache:did:example:123');
218+
expect(mockStorageService.removeItem).toHaveBeenCalledWith('did-cache:did:example:456');
219+
expect(mockStorageService.removeItem).not.toHaveBeenCalledWith('other-key:something');
220+
expect(mockStorageService.removeItem).toHaveBeenCalledTimes(2);
133221
});
134222

135-
it('should clear entire cache when no DID specified', () => {
136-
resolver.clearCache();
137-
138-
const cache = resolver.getCache();
139-
expect(Object.keys(cache)).toHaveLength(0);
140-
expect(mockStorageService.setItem).toHaveBeenCalled();
223+
it('should handle clearing cache when no cached entries exist', async () => {
224+
mockStorageService.getAllKeys.mockResolvedValue([]);
225+
226+
await resolver.clearCache();
227+
228+
expect(mockStorageService.getAllKeys).toHaveBeenCalled();
229+
expect(mockStorageService.removeItem).not.toHaveBeenCalled();
141230
});
142231
});
143232

@@ -151,34 +240,49 @@ describe('CachedDIDResolver', () => {
151240
expect(mockRouter.supports).toHaveBeenCalledWith(testId);
152241
expect(result).toBe(true);
153242
});
243+
244+
it('should return false when router does not support DID', () => {
245+
const testId = 'did:unsupported:123';
246+
mockRouter.supports.mockReturnValue(false);
247+
248+
const result = resolver.supports(testId);
249+
250+
expect(mockRouter.supports).toHaveBeenCalledWith(testId);
251+
expect(result).toBe(false);
252+
});
154253
});
155254

156-
describe('loadCache', () => {
157-
it('should load cache from storage on initialization', async () => {
255+
describe('background refresh', () => {
256+
it('should handle background refresh failure gracefully', async () => {
257+
const testDid = 'did:example:refresh-fail';
258+
const expiredTimestamp = Date.now() - (8 * 24 * 60 * 60 * 1000);
259+
const staleResult = { id: testDid, publicKey: [] };
260+
158261
const cachedData = {
159-
'did:example:123': {
160-
value: { id: 'did:example:123' },
161-
id: 'did:example:123',
162-
timestamp: Date.now(),
163-
},
262+
value: staleResult,
263+
id: testDid,
264+
timestamp: expiredTimestamp,
164265
};
165266

166267
mockStorageService.getItem.mockResolvedValue(JSON.stringify(cachedData));
167-
168-
const newResolver = new CachedDIDResolver(mockRouter);
169-
await new Promise(resolve => setTimeout(resolve, 0)); // Allow async loadCache to complete
268+
mockRouter.resolve.mockRejectedValue(new Error('Network error'));
170269

171-
expect(mockStorageService.getItem).toHaveBeenCalledWith('did-cache');
172-
expect(newResolver.getCache()).toEqual(cachedData);
173-
});
270+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
174271

175-
it('should handle empty cache from storage', async () => {
176-
mockStorageService.getItem.mockResolvedValue(null);
177-
178-
const newResolver = new CachedDIDResolver(mockRouter);
179-
await new Promise(resolve => setTimeout(resolve, 0));
272+
const result = await resolver.resolve(testDid);
273+
274+
expect(result).toEqual(staleResult);
275+
276+
// Wait for background refresh to complete
277+
await new Promise(resolve => setTimeout(resolve, 50));
278+
279+
expect(consoleSpy).toHaveBeenCalledWith(
280+
'Background refresh failed for:',
281+
testDid,
282+
expect.any(Error)
283+
);
180284

181-
expect(newResolver.getCache()).toEqual({});
285+
consoleSpy.mockRestore();
182286
});
183287
});
184288
});

packages/wasm/src/services/blockchain/cached-did-resolver.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,38 @@ export class CachedDIDResolver {
2929
private getCacheKey(did: string): string {
3030
return `${this.CACHE_PREFIX}${did}`;
3131
}
32+
3233

3334
private async getCacheEntry(did: string): Promise<CacheEntry | null> {
3435
const key = this.getCacheKey(did);
3536
const data = await storageService.getItem(key);
36-
return data ? JSON.parse(data) : null;
37+
38+
try {
39+
return data ? JSON.parse(data) : null;
40+
} catch (error) {
41+
console.error('Error parsing cache entry:', error);
42+
return null;
43+
}
3744
}
3845

3946
private async setCacheEntry(did: string, entry: CacheEntry): Promise<void> {
4047
const key = this.getCacheKey(did);
4148
await storageService.setItem(key, JSON.stringify(entry));
4249
}
4350

51+
async getCachedDIDs(): Promise<string[]> {
52+
const allKeys = await storageService.getAllKeys();
53+
const cachedDIDs: string[] = [];
54+
55+
allKeys.forEach(key => {
56+
if (key.startsWith(this.CACHE_PREFIX)) {
57+
cachedDIDs.push(key.replace(this.CACHE_PREFIX, ''));
58+
}
59+
});
60+
61+
return cachedDIDs;
62+
}
63+
4464
async resolve(did: string): Promise<any> {
4565
const cached = await this.getCacheEntry(did);
4666

@@ -58,9 +78,9 @@ export class CachedDIDResolver {
5878
console.log('Cache miss, resolving:', did);
5979
const result = await this.router.resolve(did);
6080

61-
await this.setCacheEntry(did, {
81+
await this.setCacheEntry(result.id, {
6282
value: result,
63-
id: did,
83+
id: result.id,
6484
timestamp: Date.now()
6585
});
6686

0 commit comments

Comments
 (0)