@@ -14,12 +14,22 @@ import { LoggerBase, LogId } from "./logger.js";
14
14
export const jsonExportFormat = z . enum ( [ "relaxed" , "canonical" ] ) ;
15
15
export type JSONExportFormat = z . infer < typeof jsonExportFormat > ;
16
16
17
- type StoredExport = {
17
+ interface CommonExportData {
18
18
exportName : string ;
19
19
exportURI : string ;
20
20
exportPath : string ;
21
+ }
22
+
23
+ interface ReadyExport extends CommonExportData {
24
+ exportStatus : "ready" ;
21
25
exportCreatedAt : number ;
22
- } ;
26
+ }
27
+
28
+ interface InProgressExport extends CommonExportData {
29
+ exportStatus : "in-progress" ;
30
+ }
31
+
32
+ type StoredExport = ReadyExport | InProgressExport ;
23
33
24
34
/**
25
35
* Ideally just exportName and exportURI should be made publicly available but
@@ -75,7 +85,12 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
75
85
76
86
public get availableExports ( ) : AvailableExport [ ] {
77
87
return Object . values ( this . sessionExports )
78
- . filter ( ( { exportCreatedAt : createdAt } ) => ! isExportExpired ( createdAt , this . config . exportTimeoutMs ) )
88
+ . filter ( ( sessionExport ) => {
89
+ return (
90
+ sessionExport . exportStatus === "ready" &&
91
+ ! isExportExpired ( sessionExport . exportCreatedAt , this . config . exportTimeoutMs )
92
+ ) ;
93
+ } )
79
94
. map ( ( { exportName, exportURI, exportPath } ) => ( {
80
95
exportName,
81
96
exportURI,
@@ -104,6 +119,10 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
104
119
throw new Error ( "Requested export has either expired or does not exist!" ) ;
105
120
}
106
121
122
+ if ( exportHandle . exportStatus === "in-progress" ) {
123
+ throw new Error ( "Requested export is still being generated!" ) ;
124
+ }
125
+
107
126
const { exportPath, exportCreatedAt } = exportHandle ;
108
127
109
128
if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
@@ -124,38 +143,61 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
124
143
}
125
144
}
126
145
127
- public async createJSONExport ( {
146
+ public createJSONExport ( {
128
147
input,
129
148
exportName,
130
149
jsonExportFormat,
131
150
} : {
132
151
input : FindCursor ;
133
152
exportName : string ;
134
153
jsonExportFormat : JSONExportFormat ;
135
- } ) : Promise < AvailableExport > {
154
+ } ) : AvailableExport {
136
155
try {
137
156
const exportNameWithExtension = validateExportName ( ensureExtension ( exportName , "json" ) ) ;
138
157
const exportURI = `exported-data://${ encodeURIComponent ( exportNameWithExtension ) } ` ;
139
158
const exportFilePath = path . join ( this . exportsDirectoryPath , exportNameWithExtension ) ;
159
+ const inProgressExport : InProgressExport = ( this . sessionExports [ exportNameWithExtension ] = {
160
+ exportName : exportNameWithExtension ,
161
+ exportPath : exportFilePath ,
162
+ exportURI : exportURI ,
163
+ exportStatus : "in-progress" ,
164
+ } ) ;
140
165
166
+ void this . startExport ( { input, jsonExportFormat, inProgressExport } ) ;
167
+ return inProgressExport ;
168
+ } catch ( error ) {
169
+ this . logger . error ( {
170
+ id : LogId . exportCreationError ,
171
+ context : "Error when registering JSON export request" ,
172
+ message : error instanceof Error ? error . message : String ( error ) ,
173
+ } ) ;
174
+ throw error ;
175
+ }
176
+ }
177
+
178
+ private async startExport ( {
179
+ input,
180
+ jsonExportFormat,
181
+ inProgressExport,
182
+ } : {
183
+ input : FindCursor ;
184
+ jsonExportFormat : JSONExportFormat ;
185
+ inProgressExport : InProgressExport ;
186
+ } ) : Promise < void > {
187
+ try {
141
188
await fs . mkdir ( this . exportsDirectoryPath , { recursive : true } ) ;
142
189
const inputStream = input . stream ( ) ;
143
190
const ejsonDocStream = this . docToEJSONStream ( this . getEJSONOptionsForFormat ( jsonExportFormat ) ) ;
144
- const outputStream = createWriteStream ( exportFilePath ) ;
191
+ const outputStream = createWriteStream ( inProgressExport . exportPath ) ;
145
192
outputStream . write ( "[" ) ;
146
193
let pipeSuccessful = false ;
147
194
try {
148
195
await pipeline ( [ inputStream , ejsonDocStream , outputStream ] ) ;
149
196
pipeSuccessful = true ;
150
- return {
151
- exportName,
152
- exportURI,
153
- exportPath : exportFilePath ,
154
- } ;
155
197
} catch ( pipelineError ) {
156
198
// If the pipeline errors out then we might end up with
157
199
// partial and incorrect export so we remove it entirely.
158
- await fs . unlink ( exportFilePath ) . catch ( ( error ) => {
200
+ await fs . unlink ( inProgressExport . exportPath ) . catch ( ( error ) => {
159
201
if ( ( error as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
160
202
this . logger . error ( {
161
203
id : LogId . exportCreationCleanupError ,
@@ -164,17 +206,17 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
164
206
} ) ;
165
207
}
166
208
} ) ;
209
+ delete this . sessionExports [ inProgressExport . exportName ] ;
167
210
throw pipelineError ;
168
211
} finally {
169
212
void input . close ( ) ;
170
213
if ( pipeSuccessful ) {
171
- this . sessionExports [ exportNameWithExtension ] = {
172
- exportName : exportNameWithExtension ,
214
+ this . sessionExports [ inProgressExport . exportName ] = {
215
+ ... inProgressExport ,
173
216
exportCreatedAt : Date . now ( ) ,
174
- exportPath : exportFilePath ,
175
- exportURI : exportURI ,
217
+ exportStatus : "ready" ,
176
218
} ;
177
- this . emit ( "export-available" , exportURI ) ;
219
+ this . emit ( "export-available" , inProgressExport . exportURI ) ;
178
220
}
179
221
}
180
222
} catch ( error ) {
@@ -183,7 +225,6 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
183
225
context : "Error when generating JSON export" ,
184
226
message : error instanceof Error ? error . message : String ( error ) ,
185
227
} ) ;
186
- throw error ;
187
228
}
188
229
}
189
230
@@ -228,9 +269,11 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
228
269
}
229
270
230
271
this . exportsCleanupInProgress = true ;
231
- const exportsForCleanup = { ...this . sessionExports } ;
272
+ const exportsForCleanup = Object . values ( { ...this . sessionExports } ) . filter (
273
+ ( sessionExport ) : sessionExport is ReadyExport => sessionExport . exportStatus === "ready"
274
+ ) ;
232
275
try {
233
- for ( const { exportPath, exportCreatedAt, exportURI, exportName } of Object . values ( exportsForCleanup ) ) {
276
+ for ( const { exportPath, exportCreatedAt, exportURI, exportName } of exportsForCleanup ) {
234
277
if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
235
278
delete this . sessionExports [ exportName ] ;
236
279
await this . silentlyRemoveExport ( exportPath ) ;
0 commit comments