1+ import Keyv from "keyv" ;
2+ import { describe , expect , it , beforeEach , vi , afterEach } from "vitest" ;
3+ import { MethodCacheProxy } from "./MethodCacheProxy" ;
4+
5+ describe ( "MethodCacheProxy" , ( ) => {
6+ let store : Keyv ;
7+ let testObject : any ;
8+ let cacheProxy : MethodCacheProxy < any > ;
9+
10+ beforeEach ( async ( ) => {
11+ // Use in-memory store for testing
12+ store = new Keyv ( ) ;
13+
14+ // Create a test object with various method types
15+ testObject = {
16+ syncMethod : vi . fn ( ( x : number ) => x * 2 ) ,
17+ asyncMethod : vi . fn ( async ( x : number ) => {
18+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
19+ return x * 3 ;
20+ } ) ,
21+ errorMethod : vi . fn ( async ( ) => {
22+ throw new Error ( "Test error" ) ;
23+ } ) ,
24+ nested : {
25+ deepMethod : vi . fn ( async ( x : string ) => `Hello ${ x } ` ) ,
26+ level2 : {
27+ level3Method : vi . fn ( ( x : number ) => x + 10 )
28+ }
29+ } ,
30+ counter : 0 ,
31+ incrementCounter : vi . fn ( function ( ) {
32+ this . counter ++ ;
33+ return this . counter ;
34+ } )
35+ } ;
36+ } ) ;
37+
38+ afterEach ( async ( ) => {
39+ await store . clear ( ) ;
40+ } ) ;
41+
42+ it ( "should cache method results" , async ( ) => {
43+ cacheProxy = new MethodCacheProxy ( {
44+ store,
45+ root : testObject ,
46+ } ) ;
47+
48+ const proxy = cacheProxy . getProxy ( ) ;
49+
50+ // First call - should execute the original method
51+ const result1 = await proxy . asyncMethod ( 5 ) ;
52+ expect ( result1 ) . toBe ( 15 ) ;
53+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 1 ) ;
54+
55+ // Second call with same args - should use cache
56+ const result2 = await proxy . asyncMethod ( 5 ) ;
57+ expect ( result2 ) . toBe ( 15 ) ;
58+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 1 ) ; // Still only called once
59+
60+ // Different args - should execute again
61+ const result3 = await proxy . asyncMethod ( 10 ) ;
62+ expect ( result3 ) . toBe ( 30 ) ;
63+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 2 ) ;
64+ } ) ;
65+
66+ it ( "should work with sync methods" , async ( ) => {
67+ cacheProxy = new MethodCacheProxy ( {
68+ store,
69+ root : testObject ,
70+ } ) ;
71+
72+ const proxy = cacheProxy . getProxy ( ) ;
73+
74+ const result1 = await proxy . syncMethod ( 4 ) ;
75+ expect ( result1 ) . toBe ( 8 ) ;
76+ expect ( testObject . syncMethod ) . toHaveBeenCalledTimes ( 1 ) ;
77+
78+ const result2 = await proxy . syncMethod ( 4 ) ;
79+ expect ( result2 ) . toBe ( 8 ) ;
80+ expect ( testObject . syncMethod ) . toHaveBeenCalledTimes ( 1 ) ;
81+ } ) ;
82+
83+ it ( "should handle nested objects" , async ( ) => {
84+ cacheProxy = new MethodCacheProxy ( {
85+ store,
86+ root : testObject ,
87+ } ) ;
88+
89+ const proxy = cacheProxy . getProxy ( ) ;
90+
91+ const result1 = await proxy . nested . deepMethod ( "World" ) ;
92+ expect ( result1 ) . toBe ( "Hello World" ) ;
93+ expect ( testObject . nested . deepMethod ) . toHaveBeenCalledTimes ( 1 ) ;
94+
95+ const result2 = await proxy . nested . deepMethod ( "World" ) ;
96+ expect ( result2 ) . toBe ( "Hello World" ) ;
97+ expect ( testObject . nested . deepMethod ) . toHaveBeenCalledTimes ( 1 ) ;
98+
99+ // Test deeply nested
100+ const result3 = await proxy . nested . level2 . level3Method ( 5 ) ;
101+ expect ( result3 ) . toBe ( 15 ) ;
102+ expect ( testObject . nested . level2 . level3Method ) . toHaveBeenCalledTimes ( 1 ) ;
103+
104+ const result4 = await proxy . nested . level2 . level3Method ( 5 ) ;
105+ expect ( result4 ) . toBe ( 15 ) ;
106+ expect ( testObject . nested . level2 . level3Method ) . toHaveBeenCalledTimes ( 1 ) ;
107+ } ) ;
108+
109+ it ( "should not cache errors by default" , async ( ) => {
110+ cacheProxy = new MethodCacheProxy ( {
111+ store,
112+ root : testObject ,
113+ } ) ;
114+
115+ const proxy = cacheProxy . getProxy ( ) ;
116+
117+ await expect ( proxy . errorMethod ( ) ) . rejects . toThrow ( "Test error" ) ;
118+ expect ( testObject . errorMethod ) . toHaveBeenCalledTimes ( 1 ) ;
119+
120+ // Should call again since error wasn't cached
121+ await expect ( proxy . errorMethod ( ) ) . rejects . toThrow ( "Test error" ) ;
122+ expect ( testObject . errorMethod ) . toHaveBeenCalledTimes ( 2 ) ;
123+ } ) ;
124+
125+ it ( "should use custom getKey function" , async ( ) => {
126+ const customGetKey = vi . fn ( ( path : ( string | symbol ) [ ] , args : any [ ] ) => {
127+ return `custom:${ path . join ( "/" ) } :${ JSON . stringify ( args ) } ` ;
128+ } ) ;
129+
130+ cacheProxy = new MethodCacheProxy ( {
131+ store,
132+ root : testObject ,
133+ getKey : customGetKey ,
134+ } ) ;
135+
136+ const proxy = cacheProxy . getProxy ( ) ;
137+
138+ await proxy . asyncMethod ( 5 ) ;
139+ expect ( customGetKey ) . toHaveBeenCalledWith ( [ "asyncMethod" ] , [ 5 ] ) ;
140+
141+ await proxy . nested . deepMethod ( "Test" ) ;
142+ expect ( customGetKey ) . toHaveBeenCalledWith ( [ "nested" , "deepMethod" ] , [ "Test" ] ) ;
143+ } ) ;
144+
145+ it ( "should use custom shouldCache function" , async ( ) => {
146+ const shouldCache = vi . fn ( ( path , args , result , error ) => {
147+ // Only cache if result is greater than 10
148+ return ! error && result > 10 ;
149+ } ) ;
150+
151+ cacheProxy = new MethodCacheProxy ( {
152+ store,
153+ root : testObject ,
154+ shouldCache,
155+ } ) ;
156+
157+ const proxy = cacheProxy . getProxy ( ) ;
158+
159+ // Result is 6 (3*2), should not cache
160+ await proxy . asyncMethod ( 2 ) ;
161+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 1 ) ;
162+
163+ await proxy . asyncMethod ( 2 ) ;
164+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 2 ) ; // Called again, not cached
165+
166+ // Result is 15 (5*3), should cache
167+ await proxy . asyncMethod ( 5 ) ;
168+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 3 ) ;
169+
170+ await proxy . asyncMethod ( 5 ) ;
171+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 3 ) ; // Cached
172+ } ) ;
173+
174+ it ( "should provide cache management methods" , async ( ) => {
175+ cacheProxy = new MethodCacheProxy ( {
176+ store,
177+ root : testObject ,
178+ } ) ;
179+
180+ const proxy = cacheProxy . getProxy ( ) ;
181+
182+ await proxy . asyncMethod ( 5 ) ;
183+
184+ // Test has method
185+ const key = "asyncMethod([5])" ;
186+ expect ( await cacheProxy . has ( key ) ) . toBe ( true ) ;
187+
188+ // Test get method
189+ expect ( await cacheProxy . get ( key ) ) . toBe ( 15 ) ;
190+
191+ // Test delete method
192+ await cacheProxy . delete ( key ) ;
193+ expect ( await cacheProxy . has ( key ) ) . toBe ( false ) ;
194+
195+ // Test set method
196+ await cacheProxy . set ( key , 99 ) ;
197+ expect ( await cacheProxy . get ( key ) ) . toBe ( 99 ) ;
198+
199+ // Test clear method
200+ await cacheProxy . clear ( ) ;
201+ expect ( await cacheProxy . has ( key ) ) . toBe ( false ) ;
202+ } ) ;
203+
204+ it ( "should handle concurrent calls to the same method" , async ( ) => {
205+ cacheProxy = new MethodCacheProxy ( {
206+ store,
207+ root : testObject ,
208+ } ) ;
209+
210+ const proxy = cacheProxy . getProxy ( ) ;
211+
212+ // Make concurrent calls
213+ const promises = [
214+ proxy . asyncMethod ( 7 ) ,
215+ proxy . asyncMethod ( 7 ) ,
216+ proxy . asyncMethod ( 7 ) ,
217+ ] ;
218+
219+ const results = await Promise . all ( promises ) ;
220+
221+ // All should return the same result
222+ expect ( results ) . toEqual ( [ 21 , 21 , 21 ] ) ;
223+
224+ // Method should be called 3 times (no deduplication in current implementation)
225+ // This could be improved with request deduplication
226+ expect ( testObject . asyncMethod ) . toHaveBeenCalledTimes ( 3 ) ;
227+ } ) ;
228+
229+ it ( "should maintain correct 'this' context" , async ( ) => {
230+ cacheProxy = new MethodCacheProxy ( {
231+ store,
232+ root : testObject ,
233+ } ) ;
234+
235+ const proxy = cacheProxy . getProxy ( ) ;
236+
237+ const result1 = await proxy . incrementCounter ( ) ;
238+ expect ( result1 ) . toBe ( 1 ) ;
239+ expect ( testObject . counter ) . toBe ( 1 ) ;
240+
241+ // This should be cached
242+ const result2 = await proxy . incrementCounter ( ) ;
243+ expect ( result2 ) . toBe ( 1 ) ; // Cached result
244+ expect ( testObject . counter ) . toBe ( 1 ) ; // Counter not incremented again
245+ } ) ;
246+
247+ it ( "should handle null and undefined arguments" , async ( ) => {
248+ cacheProxy = new MethodCacheProxy ( {
249+ store,
250+ root : testObject ,
251+ } ) ;
252+
253+ const proxy = cacheProxy . getProxy ( ) ;
254+
255+ testObject . nullMethod = vi . fn ( ( a : any , b : any ) => `${ a } -${ b } ` ) ;
256+
257+ const result1 = await proxy . nullMethod ( null , undefined ) ;
258+ expect ( result1 ) . toBe ( "null-undefined" ) ;
259+ expect ( testObject . nullMethod ) . toHaveBeenCalledTimes ( 1 ) ;
260+
261+ const result2 = await proxy . nullMethod ( null , undefined ) ;
262+ expect ( result2 ) . toBe ( "null-undefined" ) ;
263+ expect ( testObject . nullMethod ) . toHaveBeenCalledTimes ( 1 ) ; // Cached
264+ } ) ;
265+
266+ it ( "should handle complex argument types" , async ( ) => {
267+ cacheProxy = new MethodCacheProxy ( {
268+ store,
269+ root : testObject ,
270+ } ) ;
271+
272+ const proxy = cacheProxy . getProxy ( ) ;
273+
274+ testObject . complexMethod = vi . fn ( ( obj : any ) => obj . value * 2 ) ;
275+
276+ const arg = { value : 5 , nested : { key : "test" } } ;
277+
278+ const result1 = await proxy . complexMethod ( arg ) ;
279+ expect ( result1 ) . toBe ( 10 ) ;
280+ expect ( testObject . complexMethod ) . toHaveBeenCalledTimes ( 1 ) ;
281+
282+ // Same object structure should use cache
283+ const result2 = await proxy . complexMethod ( { value : 5 , nested : { key : "test" } } ) ;
284+ expect ( result2 ) . toBe ( 10 ) ;
285+ expect ( testObject . complexMethod ) . toHaveBeenCalledTimes ( 1 ) ;
286+
287+ // Different object should not use cache
288+ const result3 = await proxy . complexMethod ( { value : 6 , nested : { key : "test" } } ) ;
289+ expect ( result3 ) . toBe ( 12 ) ;
290+ expect ( testObject . complexMethod ) . toHaveBeenCalledTimes ( 2 ) ;
291+ } ) ;
292+ } ) ;
0 commit comments