@jolibox/implement 1.2.5-beta.3 → 1.2.5

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.
Files changed (48) hide show
  1. package/.rush/temp/package-deps_build.json +17 -24
  2. package/dist/common/cache/request-cache-service.d.ts +111 -0
  3. package/dist/common/rewards/cached-fetch-reward.d.ts +2 -2
  4. package/dist/common/rewards/cached-reward-service.d.ts +2 -2
  5. package/dist/common/rewards/index.d.ts +0 -1
  6. package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +2 -2
  7. package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +2 -2
  8. package/dist/common/rewards/reward-emitter.d.ts +0 -7
  9. package/dist/common/rewards/reward-helper.d.ts +1 -2
  10. package/dist/common/utils/index.d.ts +0 -18
  11. package/dist/h5/api/platformAdsHandle/JoliboxAdsHandler.d.ts +0 -1
  12. package/dist/index.js +9 -9
  13. package/dist/index.native.js +47 -47
  14. package/dist/native/payment/payment-service.d.ts +1 -1
  15. package/implement.build.log +2 -2
  16. package/package.json +7 -7
  17. package/src/common/cache/__tests__/request-cache-service.test.ts +686 -0
  18. package/src/common/cache/request-cache-service.ts +393 -0
  19. package/src/common/rewards/cached-fetch-reward.ts +2 -19
  20. package/src/common/rewards/cached-reward-service.ts +3 -3
  21. package/src/common/rewards/index.ts +0 -1
  22. package/src/common/rewards/registers/utils/coins/commands/use-payment.ts +8 -0
  23. package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +1 -1
  24. package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +1 -1
  25. package/src/common/rewards/reward-emitter.ts +0 -8
  26. package/src/common/rewards/reward-helper.ts +1 -8
  27. package/src/common/utils/index.ts +0 -23
  28. package/src/h5/api/ads.ts +13 -14
  29. package/src/h5/api/platformAdsHandle/JoliboxAdsHandler.ts +1 -25
  30. package/src/h5/bootstrap/index.ts +19 -4
  31. package/src/h5/rewards/index.ts +1 -18
  32. package/src/native/payment/payment-service.ts +1 -1
  33. package/CHANGELOG.json +0 -11
  34. package/CHANGELOG.md +0 -9
  35. package/dist/common/rewards/registers/use-subscription.d.ts +0 -7
  36. package/dist/common/rewards/registers/utils/subscription/commands/index.d.ts +0 -1
  37. package/dist/common/rewards/registers/utils/subscription/commands/use-subscription.d.ts +0 -4
  38. package/dist/common/rewards/registers/utils/subscription/sub-handler.d.ts +0 -13
  39. package/dist/h5/bootstrap/auth/index.d.ts +0 -2
  40. package/dist/h5/bootstrap/auth/sub.d.ts +0 -2
  41. package/src/common/rewards/registers/use-subscription.ts +0 -34
  42. package/src/common/rewards/registers/utils/subscription/commands/index.ts +0 -1
  43. package/src/common/rewards/registers/utils/subscription/commands/use-subscription.ts +0 -29
  44. package/src/common/rewards/registers/utils/subscription/sub-handler.ts +0 -88
  45. package/src/h5/bootstrap/auth/__tests__/auth.test.ts +0 -308
  46. package/src/h5/bootstrap/auth/index.ts +0 -20
  47. package/src/h5/bootstrap/auth/sub.ts +0 -56
  48. /package/dist/{h5/bootstrap/auth/__tests__/auth.test.d.ts → common/cache/__tests__/request-cache-service.test.d.ts} +0 -0
@@ -0,0 +1,686 @@
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
+ });