@@ -14,12 +14,22 @@ import { LoggerBase, LogId } from "./logger.js";
1414export const jsonExportFormat = z . enum ( [ "relaxed" , "canonical" ] ) ;
1515export type JSONExportFormat = z . infer < typeof jsonExportFormat > ;
1616
17- type StoredExport = {
17+ interface CommonExportData {
1818 exportName : string ;
1919 exportURI : string ;
2020 exportPath : string ;
21+ }
22+
23+ interface ReadyExport extends CommonExportData {
24+ exportStatus : "ready" ;
2125 exportCreatedAt : number ;
22- } ;
26+ }
27+
28+ interface InProgressExport extends CommonExportData {
29+ exportStatus : "in-progress" ;
30+ }
31+
32+ type StoredExport = ReadyExport | InProgressExport ;
2333
2434/**
2535 * Ideally just exportName and exportURI should be made publicly available but
@@ -75,7 +85,12 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
7585
7686 public get availableExports ( ) : AvailableExport [ ] {
7787 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+ } )
7994 . map ( ( { exportName, exportURI, exportPath } ) => ( {
8095 exportName,
8196 exportURI,
@@ -104,6 +119,10 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
104119 throw new Error ( "Requested export has either expired or does not exist!" ) ;
105120 }
106121
122+ if ( exportHandle . exportStatus === "in-progress" ) {
123+ throw new Error ( "Requested export is still being generated!" ) ;
124+ }
125+
107126 const { exportPath, exportCreatedAt } = exportHandle ;
108127
109128 if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
@@ -124,38 +143,61 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
124143 }
125144 }
126145
127- public async createJSONExport ( {
146+ public createJSONExport ( {
128147 input,
129148 exportName,
130149 jsonExportFormat,
131150 } : {
132151 input : FindCursor ;
133152 exportName : string ;
134153 jsonExportFormat : JSONExportFormat ;
135- } ) : Promise < AvailableExport > {
154+ } ) : AvailableExport {
136155 try {
137156 const exportNameWithExtension = validateExportName ( ensureExtension ( exportName , "json" ) ) ;
138157 const exportURI = `exported-data://${ encodeURIComponent ( exportNameWithExtension ) } ` ;
139158 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+ } ) ;
140165
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 {
141188 await fs . mkdir ( this . exportsDirectoryPath , { recursive : true } ) ;
142189 const inputStream = input . stream ( ) ;
143190 const ejsonDocStream = this . docToEJSONStream ( this . getEJSONOptionsForFormat ( jsonExportFormat ) ) ;
144- const outputStream = createWriteStream ( exportFilePath ) ;
191+ const outputStream = createWriteStream ( inProgressExport . exportPath ) ;
145192 outputStream . write ( "[" ) ;
146193 let pipeSuccessful = false ;
147194 try {
148195 await pipeline ( [ inputStream , ejsonDocStream , outputStream ] ) ;
149196 pipeSuccessful = true ;
150- return {
151- exportName,
152- exportURI,
153- exportPath : exportFilePath ,
154- } ;
155197 } catch ( pipelineError ) {
156198 // If the pipeline errors out then we might end up with
157199 // partial and incorrect export so we remove it entirely.
158- await fs . unlink ( exportFilePath ) . catch ( ( error ) => {
200+ await fs . unlink ( inProgressExport . exportPath ) . catch ( ( error ) => {
159201 if ( ( error as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
160202 this . logger . error ( {
161203 id : LogId . exportCreationCleanupError ,
@@ -164,17 +206,17 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
164206 } ) ;
165207 }
166208 } ) ;
209+ delete this . sessionExports [ inProgressExport . exportName ] ;
167210 throw pipelineError ;
168211 } finally {
169212 void input . close ( ) ;
170213 if ( pipeSuccessful ) {
171- this . sessionExports [ exportNameWithExtension ] = {
172- exportName : exportNameWithExtension ,
214+ this . sessionExports [ inProgressExport . exportName ] = {
215+ ... inProgressExport ,
173216 exportCreatedAt : Date . now ( ) ,
174- exportPath : exportFilePath ,
175- exportURI : exportURI ,
217+ exportStatus : "ready" ,
176218 } ;
177- this . emit ( "export-available" , exportURI ) ;
219+ this . emit ( "export-available" , inProgressExport . exportURI ) ;
178220 }
179221 }
180222 } catch ( error ) {
@@ -183,7 +225,6 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
183225 context : "Error when generating JSON export" ,
184226 message : error instanceof Error ? error . message : String ( error ) ,
185227 } ) ;
186- throw error ;
187228 }
188229 }
189230
@@ -228,9 +269,11 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
228269 }
229270
230271 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+ ) ;
232275 try {
233- for ( const { exportPath, exportCreatedAt, exportURI, exportName } of Object . values ( exportsForCleanup ) ) {
276+ for ( const { exportPath, exportCreatedAt, exportURI, exportName } of exportsForCleanup ) {
234277 if ( isExportExpired ( exportCreatedAt , this . config . exportTimeoutMs ) ) {
235278 delete this . sessionExports [ exportName ] ;
236279 await this . silentlyRemoveExport ( exportPath ) ;
0 commit comments