15
15
*/
16
16
17
17
import fs from 'fs' ;
18
- import { appendFile , readFile , unlink , writeFile } from 'fs/promises' ;
18
+ import { readFile , unlink , writeFile } from 'fs/promises' ;
19
19
import path from 'path' ;
20
- import * as readline from 'readline' ;
20
+ import { createInterface } from 'readline' ;
21
21
import type { ListEvalKeysRequest , ListEvalKeysResponse } from '../types/apis' ;
22
22
import {
23
23
EvalRunKeySchema ,
@@ -32,26 +32,26 @@ import { logger } from '../utils/logger';
32
32
* A local, file-based EvalStore implementation.
33
33
*/
34
34
export class LocalFileEvalStore implements EvalStore {
35
- private readonly storeRoot ;
36
- private readonly indexFile ;
37
- private readonly INDEX_DELIMITER = '\n' ;
35
+ private storeRoot : string = '' ;
36
+ private indexFile : string = '' ;
38
37
private static cachedEvalStore : LocalFileEvalStore | null = null ;
39
38
40
- private constructor ( ) {
39
+ private async init ( ) {
41
40
this . storeRoot = this . generateRootPath ( ) ;
42
- this . indexFile = this . getIndexFilePath ( ) ;
41
+ this . indexFile = await this . resolveIndexFile ( ) ;
43
42
fs . mkdirSync ( this . storeRoot , { recursive : true } ) ;
44
43
if ( ! fs . existsSync ( this . indexFile ) ) {
45
- fs . writeFileSync ( path . resolve ( this . indexFile ) , '' ) ;
44
+ fs . writeFileSync ( path . resolve ( this . indexFile ) , JSON . stringify ( { } ) ) ;
46
45
}
47
46
logger . debug (
48
47
`Initialized local file eval store at root: ${ this . storeRoot } `
49
48
) ;
50
49
}
51
50
52
- static getEvalStore ( ) {
51
+ static async getEvalStore ( ) {
53
52
if ( ! this . cachedEvalStore ) {
54
53
this . cachedEvalStore = new LocalFileEvalStore ( ) ;
54
+ await this . cachedEvalStore . init ( ) ;
55
55
}
56
56
return this . cachedEvalStore ;
57
57
}
@@ -61,7 +61,7 @@ export class LocalFileEvalStore implements EvalStore {
61
61
}
62
62
63
63
async save ( evalRun : EvalRun ) : Promise < void > {
64
- const fileName = this . generateFileName ( evalRun . key . evalRunId ) ;
64
+ const fileName = this . resolveEvalFilename ( evalRun . key . evalRunId ) ;
65
65
66
66
logger . debug (
67
67
`Saving EvalRun ${ evalRun . key . evalRunId } to ` +
@@ -72,20 +72,18 @@ export class LocalFileEvalStore implements EvalStore {
72
72
JSON . stringify ( evalRun )
73
73
) ;
74
74
75
- logger . debug (
76
- `Save EvalRunKey ${ JSON . stringify ( evalRun . key ) } to ` +
77
- path . resolve ( this . indexFile )
78
- ) ;
79
- await appendFile (
75
+ const index = await this . getEvalsIndex ( ) ;
76
+ index [ evalRun . key . evalRunId ] = evalRun . key ;
77
+ await writeFile (
80
78
path . resolve ( this . indexFile ) ,
81
- JSON . stringify ( evalRun . key ) + this . INDEX_DELIMITER
79
+ JSON . stringify ( index , null , 2 )
82
80
) ;
83
81
}
84
82
85
83
async load ( evalRunId : string ) : Promise < EvalRun | undefined > {
86
84
const filePath = path . resolve (
87
85
this . storeRoot ,
88
- this . generateFileName ( evalRunId )
86
+ this . resolveEvalFilename ( evalRunId )
89
87
) ;
90
88
if ( ! fs . existsSync ( filePath ) ) {
91
89
return undefined ;
@@ -98,22 +96,11 @@ export class LocalFileEvalStore implements EvalStore {
98
96
async list (
99
97
query ?: ListEvalKeysRequest | undefined
100
98
) : Promise < ListEvalKeysResponse > {
101
- let keys = await readFile ( this . indexFile , 'utf8' ) . then ( ( data ) => {
102
- if ( ! data ) {
103
- return [ ] ;
104
- }
105
- // strip the final carriage return before parsing all lines
106
- return data
107
- . slice ( 0 , - 1 )
108
- . split ( this . INDEX_DELIMITER )
109
- . map ( this . parseLineToKey ) ;
110
- } ) ;
111
-
112
- logger . debug ( `Found keys: ${ JSON . stringify ( keys ) } ` ) ;
99
+ logger . debug ( `Listing keys for filter: ${ JSON . stringify ( query ) } ` ) ;
100
+ let keys = await this . getEvalsIndex ( ) . then ( ( index ) => Object . values ( index ) ) ;
113
101
114
102
if ( query ?. filter ?. actionRef ) {
115
103
keys = keys . filter ( ( key ) => key . actionRef === query ?. filter ?. actionRef ) ;
116
- logger . debug ( `Filtered keys: ${ JSON . stringify ( keys ) } ` ) ;
117
104
}
118
105
119
106
return {
@@ -124,50 +111,66 @@ export class LocalFileEvalStore implements EvalStore {
124
111
async delete ( evalRunId : string ) : Promise < void > {
125
112
const filePath = path . resolve (
126
113
this . storeRoot ,
127
- this . generateFileName ( evalRunId )
114
+ this . resolveEvalFilename ( evalRunId )
128
115
) ;
129
- if ( ! fs . existsSync ( filePath ) ) {
130
- throw new Error ( `Cannot find evalRun with id '${ evalRunId } '` ) ;
116
+ if ( fs . existsSync ( filePath ) ) {
117
+ await unlink ( filePath ) ;
118
+
119
+ const index = await this . getEvalsIndex ( ) ;
120
+ delete index [ evalRunId ] ;
121
+ await writeFile (
122
+ path . resolve ( this . indexFile ) ,
123
+ JSON . stringify ( index , null , 2 )
124
+ ) ;
131
125
}
132
- return await unlink ( filePath ) . then ( ( ) =>
133
- this . deleteEvalRunFromIndex ( evalRunId )
134
- ) ;
135
126
}
136
127
137
- private generateFileName ( evalRunId : string ) : string {
128
+ private resolveEvalFilename ( evalRunId : string ) : string {
138
129
return `${ evalRunId } .json` ;
139
130
}
140
131
141
- private getIndexFilePath ( ) : string {
142
- return path . resolve ( this . storeRoot , 'index.txt' ) ;
132
+ private async resolveIndexFile ( ) : Promise < string > {
133
+ const txtPath = path . resolve ( this . storeRoot , 'index.txt' ) ;
134
+ const jsonPath = path . resolve ( this . storeRoot , 'index.json' ) ;
135
+ if ( fs . existsSync ( txtPath ) ) {
136
+ // Copy over index, delete txt file
137
+ const keys = await this . processLineByLine ( txtPath ) ;
138
+ await writeFile ( path . resolve ( jsonPath ) , JSON . stringify ( keys , null , 2 ) ) ;
139
+ await unlink ( txtPath ) ;
140
+ }
141
+ return jsonPath ;
143
142
}
144
143
145
- private parseLineToKey ( key : string ) : EvalRunKey {
146
- return EvalRunKeySchema . parse ( JSON . parse ( key ) ) ;
147
- }
144
+ private async processLineByLine ( filePath : string ) {
145
+ const fileStream = fs . createReadStream ( filePath ) ;
146
+ const keys : Record < string , EvalRunKey > = { } ;
148
147
149
- private generateRootPath ( ) : string {
150
- return path . resolve ( process . cwd ( ) , `.genkit/evals` ) ;
151
- }
152
-
153
- private async deleteEvalRunFromIndex ( evalRunId : string ) : Promise < void > {
154
- const entries = [ ] ;
155
- const fileStream = fs . createReadStream ( this . getIndexFilePath ( ) ) ;
156
- const rl = readline . createInterface ( {
148
+ const rl = createInterface ( {
157
149
input : fileStream ,
150
+ crlfDelay : Infinity ,
158
151
} ) ;
159
-
160
152
for await ( const line of rl ) {
161
- const entry = EvalRunKeySchema . parse ( JSON . parse ( line ) ) ;
162
- if ( entry . evalRunId !== evalRunId ) {
163
- entries . push ( line ) ;
153
+ try {
154
+ const entry = JSON . parse ( line ) ;
155
+ const runKey = EvalRunKeySchema . parse ( entry ) ;
156
+ keys [ runKey . evalRunId ] = runKey ;
157
+ } catch ( e ) {
158
+ logger . debug ( `Error parsing ${ line } :\n` , JSON . stringify ( e ) ) ;
164
159
}
165
160
}
161
+ return keys ;
162
+ }
166
163
167
- await writeFile (
168
- this . getIndexFilePath ( ) ,
169
- // end with delimiter to parse correctly
170
- entries . join ( this . INDEX_DELIMITER ) + this . INDEX_DELIMITER
164
+ private generateRootPath ( ) : string {
165
+ return path . resolve ( process . cwd ( ) , '.genkit' , 'evals' ) ;
166
+ }
167
+
168
+ private async getEvalsIndex ( ) : Promise < Record < string , EvalRunKey > > {
169
+ if ( ! fs . existsSync ( this . indexFile ) ) {
170
+ return Promise . resolve ( { } as any ) ;
171
+ }
172
+ return await readFile ( path . resolve ( this . indexFile ) , 'utf8' ) . then ( ( data ) =>
173
+ JSON . parse ( data )
171
174
) ;
172
175
}
173
176
}
0 commit comments