@jolibox/implement 1.2.5 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/package-deps_build.json +21 -16
- package/dist/common/rewards/cached-fetch-reward.d.ts +2 -2
- package/dist/common/rewards/cached-reward-service.d.ts +2 -2
- package/dist/common/rewards/index.d.ts +1 -0
- package/dist/common/rewards/registers/use-subscription.d.ts +7 -0
- package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +2 -2
- package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +2 -2
- package/dist/common/rewards/registers/utils/subscription/commands/index.d.ts +1 -0
- package/dist/common/rewards/registers/utils/subscription/commands/use-subscription.d.ts +4 -0
- package/dist/common/rewards/registers/utils/subscription/sub-handler.d.ts +13 -0
- package/dist/common/rewards/reward-emitter.d.ts +7 -0
- package/dist/common/rewards/reward-helper.d.ts +2 -1
- package/dist/common/utils/index.d.ts +18 -0
- package/dist/h5/api/platformAdsHandle/JoliboxAdsHandler.d.ts +1 -0
- package/dist/h5/bootstrap/auth/index.d.ts +2 -0
- package/dist/h5/bootstrap/auth/sub.d.ts +2 -0
- package/dist/index.js +9 -9
- package/dist/index.native.js +46 -46
- package/dist/native/payment/payment-service.d.ts +1 -1
- package/implement.build.log +2 -2
- package/package.json +5 -5
- package/src/common/rewards/cached-fetch-reward.ts +19 -2
- package/src/common/rewards/cached-reward-service.ts +3 -3
- package/src/common/rewards/index.ts +1 -0
- package/src/common/rewards/registers/use-subscription.ts +34 -0
- package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +1 -1
- package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +1 -1
- package/src/common/rewards/registers/utils/subscription/commands/index.ts +1 -0
- package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +29 -0
- package/src/common/rewards/registers/utils/subscription/sub-handler.ts +88 -0
- package/src/common/rewards/reward-emitter.ts +8 -0
- package/src/common/rewards/reward-helper.ts +8 -1
- package/src/common/utils/index.ts +23 -0
- package/src/h5/api/ads.ts +14 -2
- package/src/h5/api/platformAdsHandle/JoliboxAdsHandler.ts +25 -1
- package/src/h5/bootstrap/auth/__tests__/auth.test.ts +308 -0
- package/src/h5/bootstrap/auth/index.ts +20 -0
- package/src/h5/bootstrap/auth/sub.ts +56 -0
- package/src/h5/bootstrap/index.ts +4 -19
- package/src/h5/rewards/index.ts +18 -1
- package/src/native/payment/payment-service.ts +1 -1
- package/dist/common/cache/request-cache-service.d.ts +0 -111
- package/src/common/cache/__tests__/request-cache-service.test.ts +0 -686
- package/src/common/cache/request-cache-service.ts +0 -393
- /package/dist/{common/cache/__tests__/request-cache-service.test.d.ts → h5/bootstrap/auth/__tests__/auth.test.d.ts} +0 -0
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
import { RequestCacheService, RequestAdapter, CacheConfig } from '../request-cache-service';
|
|
2
|
-
|
|
3
|
-
// Mock interfaces for testing
|
|
4
|
-
interface TestRequest {
|
|
5
|
-
id: string;
|
|
6
|
-
filter?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface TestResponse {
|
|
10
|
-
status: string;
|
|
11
|
-
balance: number;
|
|
12
|
-
products: Array<{ id: string; name: string; price: number }>;
|
|
13
|
-
timestamp: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface TestCacheData {
|
|
17
|
-
products: Array<{ id: string; name: string; price: number }>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface TestRealTimeData {
|
|
21
|
-
balance: number;
|
|
22
|
-
timestamp: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Test implementation of RequestCacheService
|
|
26
|
-
class TestRequestCacheService extends RequestCacheService<
|
|
27
|
-
TestRequest,
|
|
28
|
-
TestResponse,
|
|
29
|
-
TestCacheData,
|
|
30
|
-
TestRealTimeData
|
|
31
|
-
> {
|
|
32
|
-
constructor(
|
|
33
|
-
adapter: RequestAdapter<TestRequest, TestResponse, TestCacheData, TestRealTimeData>,
|
|
34
|
-
config?: Partial<CacheConfig<TestCacheData, TestCacheData>>
|
|
35
|
-
) {
|
|
36
|
-
super(adapter, config);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Expose protected/private methods for testing
|
|
40
|
-
public forceRequest(endpoint: string, options?: TestRequest): Promise<TestResponse> {
|
|
41
|
-
return super.forceRequest(endpoint, options);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public refreshCache(endpoint: string, options?: TestRequest): Promise<TestResponse> {
|
|
45
|
-
return super.refreshCache(endpoint, options);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public warmupCache(endpoint: string, options?: TestRequest): Promise<void> {
|
|
49
|
-
return super.warmupCache(endpoint, options);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Expose new methods for testing
|
|
53
|
-
public getPendingRequestKeys(): string[] {
|
|
54
|
-
return super.getPendingRequestKeys();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public hasPendingRequest(key: string): boolean {
|
|
58
|
-
return super.hasPendingRequest(key);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public getCacheSequence(key: string): number | undefined {
|
|
62
|
-
return super.getCacheSequence(key);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public clearCache(key?: string): void {
|
|
66
|
-
return super.clearCache(key);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public clearExpiredCache(): void {
|
|
70
|
-
return super.clearExpiredCache();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public getCacheStats() {
|
|
74
|
-
return super.getCacheStats();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public getCacheKeys(): string[] {
|
|
78
|
-
return super.getCacheKeys();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public hasValidCache(key: string): boolean {
|
|
82
|
-
return super.hasValidCache(key);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Mock RequestAdapter
|
|
87
|
-
class MockRequestAdapter
|
|
88
|
-
implements RequestAdapter<TestRequest, TestResponse, TestCacheData, TestRealTimeData>
|
|
89
|
-
{
|
|
90
|
-
private mockExecute: jest.Mock;
|
|
91
|
-
private mockExtractCacheableData: jest.Mock;
|
|
92
|
-
private mockExtractRealTimeData: jest.Mock;
|
|
93
|
-
private mockMergeData: jest.Mock;
|
|
94
|
-
private mockProcessCachedData: jest.Mock;
|
|
95
|
-
|
|
96
|
-
constructor() {
|
|
97
|
-
this.mockExecute = jest.fn();
|
|
98
|
-
this.mockExtractCacheableData = jest.fn();
|
|
99
|
-
this.mockExtractRealTimeData = jest.fn();
|
|
100
|
-
this.mockMergeData = jest.fn();
|
|
101
|
-
this.mockProcessCachedData = jest.fn();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async execute(endpoint: string, options?: TestRequest): Promise<TestResponse> {
|
|
105
|
-
return this.mockExecute(endpoint, options);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
extractCacheableData(response: TestResponse): TestCacheData {
|
|
109
|
-
return this.mockExtractCacheableData(response);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
extractRealTimeData(response: TestResponse): TestRealTimeData {
|
|
113
|
-
return this.mockExtractRealTimeData(response);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
mergeData(cached: TestCacheData, realTime: TestRealTimeData): TestResponse {
|
|
117
|
-
return this.mockMergeData(cached, realTime);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
processCachedData(cached: TestCacheData): TestResponse {
|
|
121
|
-
return this.mockProcessCachedData(cached);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Getters for accessing mocks in tests
|
|
125
|
-
get executeMock() {
|
|
126
|
-
return this.mockExecute;
|
|
127
|
-
}
|
|
128
|
-
get extractCacheableDataMock() {
|
|
129
|
-
return this.mockExtractCacheableData;
|
|
130
|
-
}
|
|
131
|
-
get extractRealTimeDataMock() {
|
|
132
|
-
return this.mockExtractRealTimeData;
|
|
133
|
-
}
|
|
134
|
-
get mergeDataMock() {
|
|
135
|
-
return this.mockMergeData;
|
|
136
|
-
}
|
|
137
|
-
get processCachedDataMock() {
|
|
138
|
-
return this.mockProcessCachedData;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
describe('RequestCacheService', () => {
|
|
143
|
-
let mockAdapter: MockRequestAdapter;
|
|
144
|
-
let cacheService: TestRequestCacheService;
|
|
145
|
-
|
|
146
|
-
const mockResponse: TestResponse = {
|
|
147
|
-
status: 'success',
|
|
148
|
-
balance: 1000,
|
|
149
|
-
products: [
|
|
150
|
-
{ id: '1', name: 'Product 1', price: 100 },
|
|
151
|
-
{ id: '2', name: 'Product 2', price: 200 }
|
|
152
|
-
],
|
|
153
|
-
timestamp: Date.now()
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const mockCacheData: TestCacheData = {
|
|
157
|
-
products: [
|
|
158
|
-
{ id: '1', name: 'Product 1', price: 100 },
|
|
159
|
-
{ id: '2', name: 'Product 2', price: 200 }
|
|
160
|
-
]
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const mockRealTimeData: TestRealTimeData = {
|
|
164
|
-
balance: 1000,
|
|
165
|
-
timestamp: Date.now()
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
beforeEach(() => {
|
|
169
|
-
mockAdapter = new MockRequestAdapter();
|
|
170
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
171
|
-
duration: 5000, // 5 seconds for testing
|
|
172
|
-
timeout: 1000 // 1 second timeout
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Default mock implementations
|
|
176
|
-
mockAdapter.executeMock.mockResolvedValue(mockResponse);
|
|
177
|
-
mockAdapter.extractCacheableDataMock.mockReturnValue(mockCacheData);
|
|
178
|
-
mockAdapter.extractRealTimeDataMock.mockReturnValue(mockRealTimeData);
|
|
179
|
-
mockAdapter.mergeDataMock.mockReturnValue(mockResponse);
|
|
180
|
-
mockAdapter.processCachedDataMock.mockReturnValue(mockResponse);
|
|
181
|
-
|
|
182
|
-
// Ensure the adapter methods are properly set (not undefined)
|
|
183
|
-
mockAdapter.extractRealTimeData = mockAdapter.extractRealTimeDataMock;
|
|
184
|
-
mockAdapter.mergeData = mockAdapter.mergeDataMock;
|
|
185
|
-
|
|
186
|
-
// Clear console mocks
|
|
187
|
-
jest.spyOn(console, 'log').mockImplementation();
|
|
188
|
-
jest.spyOn(console, 'warn').mockImplementation();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
afterEach(() => {
|
|
192
|
-
jest.restoreAllMocks();
|
|
193
|
-
jest.clearAllTimers();
|
|
194
|
-
// Reset adapter methods to avoid test interference
|
|
195
|
-
if (mockAdapter) {
|
|
196
|
-
mockAdapter.extractRealTimeData = mockAdapter.extractRealTimeDataMock;
|
|
197
|
-
mockAdapter.mergeData = mockAdapter.mergeDataMock;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('Cache Miss Scenarios', () => {
|
|
202
|
-
it('should fetch fresh data when cache is empty', async () => {
|
|
203
|
-
const result = await cacheService.request('/api/test', { id: '123' });
|
|
204
|
-
|
|
205
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledWith('/api/test', { id: '123' });
|
|
206
|
-
expect(mockAdapter.extractCacheableDataMock).toHaveBeenCalledWith(mockResponse);
|
|
207
|
-
expect(result).toEqual(mockResponse);
|
|
208
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
209
|
-
'[RequestCacheService] Cache miss, fetching fresh data for /api/test (sequence: 1)'
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should cache extracted data after fresh request', async () => {
|
|
214
|
-
await cacheService.request('/api/test');
|
|
215
|
-
|
|
216
|
-
// Second request should hit cache and merge with real-time data
|
|
217
|
-
mockAdapter.executeMock.mockClear();
|
|
218
|
-
await cacheService.request('/api/test');
|
|
219
|
-
|
|
220
|
-
// Should still make network request for real-time data
|
|
221
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledTimes(1);
|
|
222
|
-
expect(mockAdapter.mergeDataMock).toHaveBeenCalledWith(mockCacheData, mockRealTimeData);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should handle when extractCacheableData returns undefined', async () => {
|
|
226
|
-
mockAdapter.extractCacheableDataMock.mockReturnValue(undefined);
|
|
227
|
-
|
|
228
|
-
const result = await cacheService.request('/api/no-cache');
|
|
229
|
-
|
|
230
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledWith('/api/no-cache', undefined);
|
|
231
|
-
expect(result).toEqual(mockResponse);
|
|
232
|
-
|
|
233
|
-
// Second request should still make network call since no cache was saved
|
|
234
|
-
mockAdapter.executeMock.mockClear();
|
|
235
|
-
await cacheService.request('/api/no-cache');
|
|
236
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledTimes(1);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should not make network request when no real-time data is needed', async () => {
|
|
240
|
-
// Create service without real-time data extractors
|
|
241
|
-
const pureAdapter = new MockRequestAdapter();
|
|
242
|
-
pureAdapter.executeMock.mockResolvedValue(mockResponse);
|
|
243
|
-
pureAdapter.extractCacheableDataMock.mockReturnValue(mockCacheData);
|
|
244
|
-
pureAdapter.processCachedDataMock.mockReturnValue(mockResponse);
|
|
245
|
-
|
|
246
|
-
// Set extractRealTimeData and mergeData to undefined to simulate pure cache scenario
|
|
247
|
-
(pureAdapter as any).extractRealTimeData = undefined;
|
|
248
|
-
(pureAdapter as any).mergeData = undefined;
|
|
249
|
-
|
|
250
|
-
const pureCacheService = new TestRequestCacheService(pureAdapter, {
|
|
251
|
-
duration: 5000,
|
|
252
|
-
timeout: 1000
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// First request - should make network call
|
|
256
|
-
await pureCacheService.request('/api/pure-cache');
|
|
257
|
-
|
|
258
|
-
// Second request - should NOT make network call
|
|
259
|
-
pureAdapter.executeMock.mockClear();
|
|
260
|
-
const result = await pureCacheService.request('/api/pure-cache');
|
|
261
|
-
|
|
262
|
-
expect(pureAdapter.executeMock).not.toHaveBeenCalled();
|
|
263
|
-
expect(pureAdapter.processCachedDataMock).toHaveBeenCalledWith(mockCacheData);
|
|
264
|
-
expect(result).toEqual(mockResponse);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should make fresh request when cache is expired', async () => {
|
|
268
|
-
jest.useFakeTimers();
|
|
269
|
-
|
|
270
|
-
// Create service with very short cache duration
|
|
271
|
-
const shortCacheService = new TestRequestCacheService(mockAdapter, {
|
|
272
|
-
duration: 1000, // 1 second
|
|
273
|
-
timeout: 1000
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// First request
|
|
277
|
-
await shortCacheService.request('/api/expire-test');
|
|
278
|
-
mockAdapter.executeMock.mockClear();
|
|
279
|
-
|
|
280
|
-
// Fast-forward time to expire cache
|
|
281
|
-
jest.advanceTimersByTime(1500);
|
|
282
|
-
|
|
283
|
-
// Second request should make network call since cache expired
|
|
284
|
-
await shortCacheService.request('/api/expire-test');
|
|
285
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledTimes(1);
|
|
286
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
287
|
-
'[RequestCacheService] Cache miss, fetching fresh data for /api/expire-test (sequence: 2)'
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
jest.useRealTimers();
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('Cache Hit Scenarios', () => {
|
|
295
|
-
beforeEach(async () => {
|
|
296
|
-
// Pre-populate cache
|
|
297
|
-
await cacheService.request('/api/test');
|
|
298
|
-
mockAdapter.executeMock.mockClear();
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should return cached data without network request for pure cache scenario', async () => {
|
|
302
|
-
// Set extractRealTimeData and mergeData to undefined to simulate pure cache scenario
|
|
303
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
304
|
-
(mockAdapter as any).mergeData = undefined;
|
|
305
|
-
|
|
306
|
-
const result = await cacheService.request('/api/test');
|
|
307
|
-
|
|
308
|
-
expect(mockAdapter.executeMock).not.toHaveBeenCalled();
|
|
309
|
-
expect(mockAdapter.processCachedDataMock).toHaveBeenCalledWith(mockCacheData);
|
|
310
|
-
expect(result).toEqual(mockResponse);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('should merge cached data with fresh real-time data', async () => {
|
|
314
|
-
const result = await cacheService.request('/api/test');
|
|
315
|
-
|
|
316
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledTimes(1);
|
|
317
|
-
expect(mockAdapter.extractRealTimeDataMock).toHaveBeenCalledWith(mockResponse);
|
|
318
|
-
expect(mockAdapter.mergeDataMock).toHaveBeenCalledWith(mockCacheData, mockRealTimeData);
|
|
319
|
-
expect(result).toEqual(mockResponse);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
describe('Cache Invalidation', () => {
|
|
324
|
-
beforeEach(async () => {
|
|
325
|
-
// Pre-populate cache
|
|
326
|
-
await cacheService.request('/api/test');
|
|
327
|
-
mockAdapter.executeMock.mockClear();
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('should invalidate cache when consistency check fails', async () => {
|
|
331
|
-
// Setup service with consistency checker that returns false
|
|
332
|
-
const consistencyChecker = jest.fn().mockReturnValue(false);
|
|
333
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
334
|
-
duration: 5000,
|
|
335
|
-
consistencyChecker
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Pre-populate cache
|
|
339
|
-
await cacheService.request('/api/test');
|
|
340
|
-
mockAdapter.executeMock.mockClear();
|
|
341
|
-
|
|
342
|
-
const newCacheData = { products: [{ id: '3', name: 'New Product', price: 300 }] };
|
|
343
|
-
mockAdapter.extractCacheableDataMock.mockReturnValue(newCacheData);
|
|
344
|
-
|
|
345
|
-
const result = await cacheService.request('/api/test');
|
|
346
|
-
|
|
347
|
-
expect(consistencyChecker).toHaveBeenCalled();
|
|
348
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
349
|
-
'[RequestCacheService] Cache invalidated due to data change for /api/test'
|
|
350
|
-
);
|
|
351
|
-
expect(result).toEqual(mockResponse);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should keep cache when consistency check passes', async () => {
|
|
355
|
-
// Setup service with consistency checker that returns true
|
|
356
|
-
const consistencyChecker = jest.fn().mockReturnValue(true);
|
|
357
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
358
|
-
duration: 5000,
|
|
359
|
-
consistencyChecker
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// Pre-populate cache
|
|
363
|
-
await cacheService.request('/api/test');
|
|
364
|
-
mockAdapter.executeMock.mockClear();
|
|
365
|
-
|
|
366
|
-
const result = await cacheService.request('/api/test');
|
|
367
|
-
|
|
368
|
-
expect(consistencyChecker).toHaveBeenCalledWith(mockCacheData, mockCacheData);
|
|
369
|
-
expect(mockAdapter.mergeDataMock).toHaveBeenCalledWith(mockCacheData, mockRealTimeData);
|
|
370
|
-
expect(result).toEqual(mockResponse);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('should not run consistency check when no server cacheable data', async () => {
|
|
374
|
-
const consistencyChecker = jest.fn();
|
|
375
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
376
|
-
duration: 5000,
|
|
377
|
-
consistencyChecker
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Pre-populate cache
|
|
381
|
-
await cacheService.request('/api/test');
|
|
382
|
-
mockAdapter.executeMock.mockClear();
|
|
383
|
-
|
|
384
|
-
// Set extractCacheableData to return undefined
|
|
385
|
-
mockAdapter.extractCacheableDataMock.mockReturnValue(undefined);
|
|
386
|
-
|
|
387
|
-
const result = await cacheService.request('/api/test');
|
|
388
|
-
|
|
389
|
-
expect(consistencyChecker).not.toHaveBeenCalled();
|
|
390
|
-
expect(mockAdapter.mergeDataMock).toHaveBeenCalledWith(mockCacheData, mockRealTimeData);
|
|
391
|
-
expect(result).toEqual(mockResponse);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
describe('Error Handling', () => {
|
|
396
|
-
beforeEach(async () => {
|
|
397
|
-
// Pre-populate cache
|
|
398
|
-
await cacheService.request('/api/test');
|
|
399
|
-
mockAdapter.executeMock.mockClear();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('should fallback to cached data when real-time request fails', async () => {
|
|
403
|
-
mockAdapter.executeMock.mockRejectedValue(new Error('Network error'));
|
|
404
|
-
|
|
405
|
-
const result = await cacheService.request('/api/test');
|
|
406
|
-
|
|
407
|
-
expect(mockAdapter.processCachedDataMock).toHaveBeenCalledWith(mockCacheData);
|
|
408
|
-
expect(result).toEqual(mockResponse);
|
|
409
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
410
|
-
'[RequestCacheService] Failed to fetch real-time data, using cache only for /api/test:',
|
|
411
|
-
expect.any(Error)
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('should handle timeout', async () => {
|
|
416
|
-
jest.useFakeTimers();
|
|
417
|
-
|
|
418
|
-
mockAdapter.executeMock.mockImplementation(
|
|
419
|
-
() => new Promise((resolve) => setTimeout(() => resolve(mockResponse), 2000))
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const requestPromise = cacheService.request('/api/test-timeout');
|
|
423
|
-
|
|
424
|
-
// Fast-forward time to trigger timeout
|
|
425
|
-
jest.advanceTimersByTime(1000);
|
|
426
|
-
|
|
427
|
-
await expect(requestPromise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
|
|
428
|
-
|
|
429
|
-
jest.useRealTimers();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('should allow background cache update even when timeout occurs', async () => {
|
|
433
|
-
jest.useFakeTimers();
|
|
434
|
-
|
|
435
|
-
// Mock a slow request that completes after timeout
|
|
436
|
-
let resolveRequest: (value: any) => void;
|
|
437
|
-
const slowRequestPromise = new Promise((resolve) => {
|
|
438
|
-
resolveRequest = resolve;
|
|
439
|
-
});
|
|
440
|
-
mockAdapter.executeMock.mockImplementation(() => slowRequestPromise);
|
|
441
|
-
|
|
442
|
-
// Start the request
|
|
443
|
-
const requestPromise = cacheService.request('/api/background-test');
|
|
444
|
-
|
|
445
|
-
// Advance time to trigger timeout
|
|
446
|
-
jest.advanceTimersByTime(1000);
|
|
447
|
-
|
|
448
|
-
// Request should timeout
|
|
449
|
-
await expect(requestPromise).rejects.toThrow('[RequestCacheService] Request timeout after 1 seconds');
|
|
450
|
-
|
|
451
|
-
// Verify cache is initially empty
|
|
452
|
-
expect(cacheService.hasValidCache('/api/background-test')).toBe(false);
|
|
453
|
-
|
|
454
|
-
// Now let the background request complete
|
|
455
|
-
resolveRequest!(mockResponse);
|
|
456
|
-
|
|
457
|
-
// Use real timers for the async wait
|
|
458
|
-
jest.useRealTimers();
|
|
459
|
-
|
|
460
|
-
// Wait for the background promise to resolve
|
|
461
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
462
|
-
|
|
463
|
-
// Verify that cache was updated in the background
|
|
464
|
-
expect(cacheService.hasValidCache('/api/background-test')).toBe(true);
|
|
465
|
-
}, 10000);
|
|
466
|
-
|
|
467
|
-
it('should fallback to cache when timeout occurs and cache exists', async () => {
|
|
468
|
-
jest.useFakeTimers();
|
|
469
|
-
|
|
470
|
-
// Pre-populate cache
|
|
471
|
-
await cacheService.request('/api/fallback-test');
|
|
472
|
-
mockAdapter.executeMock.mockClear();
|
|
473
|
-
|
|
474
|
-
// Mock a slow request
|
|
475
|
-
mockAdapter.executeMock.mockImplementation(
|
|
476
|
-
() => new Promise((resolve) => setTimeout(() => resolve(mockResponse), 2000))
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
// Set extractRealTimeData and mergeData to undefined to test pure cache scenario
|
|
480
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
481
|
-
(mockAdapter as any).mergeData = undefined;
|
|
482
|
-
|
|
483
|
-
const requestPromise = cacheService.request('/api/fallback-test');
|
|
484
|
-
|
|
485
|
-
// Fast-forward time to trigger timeout
|
|
486
|
-
jest.advanceTimersByTime(1000);
|
|
487
|
-
|
|
488
|
-
// Should return cached data instead of throwing timeout error
|
|
489
|
-
const result = await requestPromise;
|
|
490
|
-
expect(result).toEqual(mockResponse);
|
|
491
|
-
expect(mockAdapter.processCachedDataMock).toHaveBeenCalledWith(mockCacheData);
|
|
492
|
-
|
|
493
|
-
jest.useRealTimers();
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
describe('Cache-Only Operations', () => {
|
|
498
|
-
it('should return cached data when cache exists', async () => {
|
|
499
|
-
// Pre-populate cache first
|
|
500
|
-
await cacheService.request('/api/test');
|
|
501
|
-
mockAdapter.executeMock.mockClear();
|
|
502
|
-
|
|
503
|
-
// Set extractRealTimeData and mergeData to undefined to simulate pure cache scenario
|
|
504
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
505
|
-
(mockAdapter as any).mergeData = undefined;
|
|
506
|
-
|
|
507
|
-
const result = await cacheService.request('/api/test');
|
|
508
|
-
|
|
509
|
-
expect(mockAdapter.executeMock).not.toHaveBeenCalled();
|
|
510
|
-
expect(mockAdapter.processCachedDataMock).toHaveBeenCalledWith(mockCacheData);
|
|
511
|
-
expect(result).toEqual(mockResponse);
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
describe('Force Request', () => {
|
|
516
|
-
it('should always make network request and update cache', async () => {
|
|
517
|
-
// Pre-populate cache
|
|
518
|
-
await cacheService.request('/api/test');
|
|
519
|
-
|
|
520
|
-
// Verify that normal request would not make network call (pure cache scenario)
|
|
521
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
522
|
-
(mockAdapter as any).mergeData = undefined;
|
|
523
|
-
mockAdapter.executeMock.mockClear();
|
|
524
|
-
|
|
525
|
-
await cacheService.request('/api/test');
|
|
526
|
-
expect(mockAdapter.executeMock).not.toHaveBeenCalled(); // 验证缓存有效,没有网络请求
|
|
527
|
-
|
|
528
|
-
// Now test force request - should always make network call
|
|
529
|
-
mockAdapter.executeMock.mockClear();
|
|
530
|
-
const result = await cacheService.forceRequest('/api/test');
|
|
531
|
-
|
|
532
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledTimes(1);
|
|
533
|
-
expect(result).toEqual(mockResponse);
|
|
534
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
535
|
-
'[RequestCacheService] Force request for /api/test (sequence: 3)'
|
|
536
|
-
);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
describe('Cache Management', () => {
|
|
541
|
-
beforeEach(async () => {
|
|
542
|
-
// Pre-populate cache
|
|
543
|
-
await cacheService.request('/api/test1');
|
|
544
|
-
await cacheService.request('/api/test2');
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('should refresh cache by deleting and re-requesting', async () => {
|
|
548
|
-
const result = await cacheService.refreshCache('/api/test1');
|
|
549
|
-
|
|
550
|
-
expect(result).toEqual(mockResponse);
|
|
551
|
-
// Should have made network request for refresh
|
|
552
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('should clear specific cache entry', async () => {
|
|
556
|
-
cacheService.clearCache('/api/test1');
|
|
557
|
-
|
|
558
|
-
// Try to get cached data - should trigger network request since cache was cleared
|
|
559
|
-
mockAdapter.executeMock.mockClear();
|
|
560
|
-
await cacheService.request('/api/test1');
|
|
561
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
562
|
-
|
|
563
|
-
// Other cache should still exist - should not trigger network request
|
|
564
|
-
mockAdapter.executeMock.mockClear();
|
|
565
|
-
// Set extractRealTimeData and mergeData to undefined to test pure cache
|
|
566
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
567
|
-
(mockAdapter as any).mergeData = undefined;
|
|
568
|
-
|
|
569
|
-
await cacheService.request('/api/test2');
|
|
570
|
-
expect(mockAdapter.executeMock).not.toHaveBeenCalled();
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
it('should clear all cache when no key provided', async () => {
|
|
574
|
-
cacheService.clearCache();
|
|
575
|
-
|
|
576
|
-
// Both should trigger network requests since cache was cleared
|
|
577
|
-
mockAdapter.executeMock.mockClear();
|
|
578
|
-
await cacheService.request('/api/test1');
|
|
579
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
580
|
-
|
|
581
|
-
mockAdapter.executeMock.mockClear();
|
|
582
|
-
await cacheService.request('/api/test2');
|
|
583
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('should get cache statistics', () => {
|
|
587
|
-
const stats = cacheService.getCacheStats();
|
|
588
|
-
|
|
589
|
-
expect(stats.cacheCount).toBe(2);
|
|
590
|
-
expect(stats.validCount).toBe(2);
|
|
591
|
-
expect(stats.expiredCount).toBe(0);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it('should get cache keys', () => {
|
|
595
|
-
const keys = cacheService.getCacheKeys();
|
|
596
|
-
|
|
597
|
-
expect(keys).toContain('/api/test1');
|
|
598
|
-
expect(keys).toContain('/api/test2');
|
|
599
|
-
expect(keys.length).toBe(2);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('should check if cache is valid', () => {
|
|
603
|
-
expect(cacheService.hasValidCache('/api/test1')).toBe(true);
|
|
604
|
-
expect(cacheService.hasValidCache('/api/nonexistent')).toBe(false);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it('should clear expired cache entries', async () => {
|
|
608
|
-
jest.useFakeTimers();
|
|
609
|
-
|
|
610
|
-
// Fast-forward time to expire cache
|
|
611
|
-
jest.advanceTimersByTime(6000); // More than 5 second cache duration
|
|
612
|
-
|
|
613
|
-
cacheService.clearExpiredCache();
|
|
614
|
-
|
|
615
|
-
// Both should trigger network requests since cache expired and was cleared
|
|
616
|
-
mockAdapter.executeMock.mockClear();
|
|
617
|
-
await cacheService.request('/api/test1');
|
|
618
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
619
|
-
|
|
620
|
-
mockAdapter.executeMock.mockClear();
|
|
621
|
-
await cacheService.request('/api/test2');
|
|
622
|
-
expect(mockAdapter.executeMock).toHaveBeenCalled();
|
|
623
|
-
|
|
624
|
-
jest.useRealTimers();
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
describe('Cache Warmup', () => {
|
|
629
|
-
it('should warmup cache successfully', async () => {
|
|
630
|
-
await cacheService.warmupCache('/api/warmup');
|
|
631
|
-
|
|
632
|
-
expect(mockAdapter.executeMock).toHaveBeenCalledWith('/api/warmup', undefined);
|
|
633
|
-
|
|
634
|
-
// Subsequent request should hit cache (no network request)
|
|
635
|
-
mockAdapter.executeMock.mockClear();
|
|
636
|
-
// Set extractRealTimeData and mergeData to undefined to test pure cache
|
|
637
|
-
(mockAdapter as any).extractRealTimeData = undefined;
|
|
638
|
-
(mockAdapter as any).mergeData = undefined;
|
|
639
|
-
|
|
640
|
-
await cacheService.request('/api/warmup');
|
|
641
|
-
expect(mockAdapter.executeMock).not.toHaveBeenCalled();
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('should handle warmup failure gracefully', async () => {
|
|
645
|
-
mockAdapter.executeMock.mockRejectedValue(new Error('Warmup failed'));
|
|
646
|
-
|
|
647
|
-
await expect(cacheService.warmupCache('/api/warmup')).resolves.not.toThrow();
|
|
648
|
-
|
|
649
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
650
|
-
'[RequestCacheService] Cache warmup failed for /api/warmup:',
|
|
651
|
-
expect.any(Error)
|
|
652
|
-
);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
describe('Custom Configuration', () => {
|
|
657
|
-
it('should use custom key generator', async () => {
|
|
658
|
-
const customKeyGenerator = jest.fn().mockReturnValue('custom-key');
|
|
659
|
-
|
|
660
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
661
|
-
keyGenerator: customKeyGenerator
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
await cacheService.request('/api/test', { id: '123', filter: 'active' });
|
|
665
|
-
|
|
666
|
-
expect(customKeyGenerator).toHaveBeenCalledWith('/api/test', { id: '123', filter: 'active' });
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('should use custom consistency checker', async () => {
|
|
670
|
-
const customConsistencyChecker = jest.fn().mockReturnValue(true);
|
|
671
|
-
|
|
672
|
-
cacheService = new TestRequestCacheService(mockAdapter, {
|
|
673
|
-
consistencyChecker: customConsistencyChecker
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
// Pre-populate cache
|
|
677
|
-
await cacheService.request('/api/test');
|
|
678
|
-
mockAdapter.executeMock.mockClear();
|
|
679
|
-
|
|
680
|
-
// Second request should trigger consistency check
|
|
681
|
-
await cacheService.request('/api/test');
|
|
682
|
-
|
|
683
|
-
expect(customConsistencyChecker).toHaveBeenCalledWith(mockCacheData, mockCacheData);
|
|
684
|
-
});
|
|
685
|
-
});
|
|
686
|
-
});
|