1+ import fs from "fs" ;
2+ import path from "path" ;
3+ import { DecodedProto } from "../types" ;
4+
5+ interface SampleConfig {
6+ enabled : boolean ;
7+ save_directory : string ;
8+ max_samples_per_method : number ;
9+ endpoints : string [ ] ;
10+ }
11+
12+ interface SavedSample {
13+ methodId : string ;
14+ methodName : string ;
15+ timestamp : string ;
16+ request : any ;
17+ response : any ;
18+ }
19+
20+ interface SampleFile {
21+ filename : string ;
22+ filepath : string ;
23+ timestamp : number ;
24+ methodId : string ;
25+ }
26+
27+ class SampleSaver {
28+ private config : SampleConfig ;
29+ private savedSamples : Map < string , SampleFile [ ] > = new Map ( ) ;
30+ private saveDirectory : string ;
31+
32+ constructor ( config : SampleConfig ) {
33+ this . config = config ;
34+ this . saveDirectory = path . resolve ( config . save_directory ) ;
35+ this . initializeStorage ( ) ;
36+ this . loadExistingSamples ( ) ;
37+ }
38+
39+ private initializeStorage ( ) : void {
40+ if ( ! this . config . enabled ) return ;
41+
42+ if ( ! fs . existsSync ( this . saveDirectory ) ) {
43+ fs . mkdirSync ( this . saveDirectory , { recursive : true } ) ;
44+ console . log ( `Created sample storage directory: ${ this . saveDirectory } ` ) ;
45+ }
46+ }
47+
48+ private loadExistingSamples ( ) : void {
49+ if ( ! this . config . enabled ) return ;
50+
51+ if ( fs . existsSync ( this . saveDirectory ) ) {
52+ const files = fs . readdirSync ( this . saveDirectory ) ;
53+
54+ files . forEach ( file => {
55+ const match = file . match ( / ^ ( \d + ) _ .* _ ( \d + ) \. j s o n $ / ) ;
56+ if ( match ) {
57+ const methodId = match [ 1 ] ;
58+ const timestamp = parseInt ( match [ 2 ] ) ;
59+ const filepath = path . join ( this . saveDirectory , file ) ;
60+
61+ const samples = this . savedSamples . get ( methodId ) || [ ] ;
62+ samples . push ( {
63+ filename : file ,
64+ filepath : filepath ,
65+ timestamp : timestamp ,
66+ methodId : methodId
67+ } ) ;
68+ this . savedSamples . set ( methodId , samples ) ;
69+ }
70+ } ) ;
71+
72+ // Sort samples by timestamp for each method
73+ for ( const [ , samples ] of this . savedSamples . entries ( ) ) {
74+ samples . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
75+ }
76+ }
77+ }
78+
79+ private getTimestamp ( ) : string {
80+ return new Date ( ) . toISOString ( ) ;
81+ }
82+
83+ private deleteOldestSample ( methodId : string ) : void {
84+ const samples = this . savedSamples . get ( methodId ) ;
85+ if ( samples && samples . length > 0 ) {
86+ const oldest = samples . shift ( ) ;
87+ if ( oldest ) {
88+ try {
89+ fs . unlinkSync ( oldest . filepath ) ;
90+ console . log ( `Deleted oldest sample: ${ oldest . filename } ` ) ;
91+ } catch ( error ) {
92+ console . error ( `Failed to delete old sample: ${ error } ` ) ;
93+ }
94+ }
95+ }
96+ }
97+
98+ private shouldSave ( endpoint : string ) : boolean {
99+ if ( ! this . config . enabled ) return false ;
100+ if ( ! this . config . endpoints . includes ( endpoint ) ) return false ;
101+ return true ;
102+ }
103+
104+ public async saveSample ( request : DecodedProto , response : DecodedProto | null , endpoint : string ) : Promise < void > {
105+ if ( ! request || ! request . methodId ) return ;
106+ if ( ! this . shouldSave ( endpoint ) ) return ;
107+
108+ const methodSamples = this . savedSamples . get ( request . methodId ) || [ ] ;
109+
110+ // If we've reached the max samples for this method, delete the oldest
111+ if ( methodSamples . length >= this . config . max_samples_per_method ) {
112+ this . deleteOldestSample ( request . methodId ) ;
113+ }
114+
115+ const sample : SavedSample = {
116+ methodId : request . methodId ,
117+ methodName : request . methodName ,
118+ timestamp : this . getTimestamp ( ) ,
119+ request : request . data ,
120+ response : response ? response . data : null
121+ } ;
122+
123+ const safeMethodName = request . methodName . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_' ) ;
124+ const timestamp = Date . now ( ) ;
125+ const filename = `${ request . methodId } _${ safeMethodName } _${ timestamp } .json` ;
126+ const filepath = path . join ( this . saveDirectory , filename ) ;
127+
128+ try {
129+ fs . writeFileSync ( filepath , JSON . stringify ( sample , null , 2 ) ) ;
130+
131+ // Update our tracking
132+ const samples = this . savedSamples . get ( request . methodId ) || [ ] ;
133+ samples . push ( {
134+ filename : filename ,
135+ filepath : filepath ,
136+ timestamp : timestamp ,
137+ methodId : request . methodId
138+ } ) ;
139+ this . savedSamples . set ( request . methodId , samples ) ;
140+
141+ console . log ( `Saved sample for method ${ request . methodId } (${ request . methodName } ): ${ filename } ` ) ;
142+ } catch ( error ) {
143+ console . error ( `Failed to save sample: ${ error } ` ) ;
144+ }
145+ }
146+
147+ public async savePair ( request : DecodedProto , response : DecodedProto , endpoint : string ) : Promise < void > {
148+ await this . saveSample ( request , response , endpoint ) ;
149+ }
150+ }
151+
152+ export default SampleSaver ;
0 commit comments