11import { EventEmitter } from 'events'
22import { randomUUID } from 'crypto'
3+ import { spawn } from 'child_process'
4+ import * as path from 'path'
5+ import * as fs from 'fs-extra'
6+ import { tmpdir } from 'os'
37
48export interface ExportJob {
59 id : string
@@ -26,7 +30,9 @@ export interface ExportJob {
2630export class JobQueue extends EventEmitter {
2731 private queue : ExportJob [ ] = [ ]
2832 private currentJob : ExportJob | null = null
33+ private completedJobs : Map < string , ExportJob > = new Map ( )
2934 private isProcessing = false
35+ private maxCompletedJobs = 100 // Keep last 100 completed jobs
3036
3137 addJob ( jobData : Omit < ExportJob , 'id' | 'status' | 'createdAt' > ) : {
3238 jobId : string
@@ -59,7 +65,11 @@ export class JobQueue extends EventEmitter {
5965 if ( this . currentJob ?. id === jobId ) {
6066 return this . currentJob
6167 }
62- return this . queue . find ( ( job ) => job . id === jobId )
68+ const queuedJob = this . queue . find ( ( job ) => job . id === jobId )
69+ if ( queuedJob ) {
70+ return queuedJob
71+ }
72+ return this . completedJobs . get ( jobId )
6373 }
6474
6575 getQueuePosition ( jobId : string ) : number {
@@ -95,6 +105,17 @@ export class JobQueue extends EventEmitter {
95105 this . currentJob . completedAt = new Date ( )
96106 this . emit ( 'job-failed' , this . currentJob )
97107 } finally {
108+ // Store completed job
109+ if ( this . currentJob ) {
110+ this . completedJobs . set ( this . currentJob . id , this . currentJob )
111+
112+ // Cleanup old completed jobs if limit exceeded
113+ if ( this . completedJobs . size > this . maxCompletedJobs ) {
114+ const firstKey = this . completedJobs . keys ( ) . next ( ) . value
115+ this . completedJobs . delete ( firstKey )
116+ }
117+ }
118+
98119 this . currentJob = null
99120 this . isProcessing = false
100121
@@ -106,17 +127,163 @@ export class JobQueue extends EventEmitter {
106127 }
107128
108129 private async performExport ( job : ExportJob ) : Promise < void > {
109- // This is where the actual export logic would go
110- // For now, simulate processing time
111- return new Promise ( ( resolve ) => {
112- setTimeout ( ( ) => {
130+ return new Promise ( async ( resolve , reject ) => {
131+ try {
132+ // Determine input file
133+ let inputFile : string
134+ if (
135+ job . source . type === 'upload' &&
136+ job . source . files &&
137+ job . source . files . length > 0
138+ ) {
139+ // Use the first file (README.md or similar)
140+ const readmeFile = job . source . files . find (
141+ ( f ) =>
142+ f . filename === 'README.md' ||
143+ f . filename . toLowerCase ( ) . endsWith ( '.md' )
144+ )
145+ inputFile = readmeFile ? readmeFile . path : job . source . files [ 0 ] . path
146+ } else if ( job . source . type === 'git' && job . source . gitUrl ) {
147+ // For git repos, we'd need to clone first - not implemented yet
148+ throw new Error ( 'Git repository export not yet implemented' )
149+ } else {
150+ throw new Error ( 'No valid input source' )
151+ }
152+
153+ // Determine format based on preset or format
154+ let format : string
155+ if ( job . target . preset ) {
156+ // Map presets to formats
157+ const presetMap : Record < string , string > = {
158+ moodle : 'scorm2004' ,
159+ ilias : 'scorm2004' ,
160+ opal : 'scorm2004' ,
161+ generic : 'scorm2004' ,
162+ openolat : 'scorm2004' ,
163+ openedx : 'scorm2004' ,
164+ }
165+ format = presetMap [ job . target . preset ] || 'scorm2004'
166+ } else {
167+ format = job . target . format || 'web'
168+ }
169+
170+ // Create output directory
171+ const outputDir = path . join ( tmpdir ( ) , 'liaex-exports' , job . id )
172+ await fs . ensureDir ( outputDir )
173+
174+ const outputFile = path . join ( outputDir , 'export' )
175+
176+ // Convert options to proper types and build CLI arguments
177+ const args : string [ ] = [
178+ '--input' ,
179+ inputFile ,
180+ '--format' ,
181+ format ,
182+ '--output' ,
183+ outputFile ,
184+ ]
185+
186+ for ( const [ key , value ] of Object . entries ( job . options || { } ) ) {
187+ // Convert option keys back to CLI format (e.g., 'scorm-masteryScore' -> '--scorm-masteryScore')
188+ const cliKey = `--${ key . replace ( / _ / g, '-' ) } `
189+
190+ // Convert string booleans to actual booleans
191+ if ( value === 'true' ) {
192+ args . push ( cliKey )
193+ } else if ( value === 'false' ) {
194+ // Don't add false flags
195+ } else if ( value ) {
196+ args . push ( cliKey , String ( value ) )
197+ }
198+ }
199+
200+ // Run export in separate process using the CLI
201+ // Use the current process's argv[1] which is the path to dist/index.js
202+ const cliPath = process . argv [ 1 ]
113203 console . log (
114- `Exporting job ${ job . id } with target ${
115- job . target . preset || job . target . format
116- } `
204+ `Starting export process for job ${
205+ job . id
206+ } : node ${ cliPath } ${ args . join ( ' ' ) } `
117207 )
118- resolve ( )
119- } , 5000 ) // 5 second delay to simulate export
208+
209+ const exportProcess = spawn ( 'node' , [ cliPath , ...args ] , {
210+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
211+ detached : false ,
212+ } )
213+
214+ let stdout = ''
215+ let stderr = ''
216+
217+ exportProcess . stdout ?. on ( 'data' , ( data ) => {
218+ stdout += data . toString ( )
219+ console . log ( `[Job ${ job . id } ] ${ data . toString ( ) . trim ( ) } ` )
220+ } )
221+
222+ exportProcess . stderr ?. on ( 'data' , ( data ) => {
223+ stderr += data . toString ( )
224+ console . error ( `[Job ${ job . id } ] ${ data . toString ( ) . trim ( ) } ` )
225+ } )
226+
227+ exportProcess . on ( 'error' , ( error ) => {
228+ console . error ( `Export process error for job ${ job . id } :` , error )
229+ reject ( error )
230+ } )
231+
232+ exportProcess . on ( 'close' , async ( code ) => {
233+ if ( code !== 0 ) {
234+ reject (
235+ new Error (
236+ `Export process exited with code ${ code } . stderr: ${ stderr } `
237+ )
238+ )
239+ return
240+ }
241+
242+ try {
243+ // Give the export process a moment to finish writing files
244+ await new Promise ( ( resolveTimeout ) =>
245+ setTimeout ( resolveTimeout , 1000 )
246+ )
247+
248+ // Find the generated output file
249+ const files = await fs . readdir ( outputDir )
250+ console . log ( `Files in output directory: ${ files . join ( ', ' ) } ` )
251+
252+ const outputFileName = files . find (
253+ ( f ) =>
254+ f . startsWith ( 'export' ) &&
255+ ( f . endsWith ( '.zip' ) ||
256+ f . endsWith ( '.html' ) ||
257+ f . endsWith ( '.pdf' ) ||
258+ f . endsWith ( '.epub' ) )
259+ )
260+
261+ if ( ! outputFileName ) {
262+ throw new Error (
263+ `Export completed but no output file was generated. Found files: ${ files . join (
264+ ', '
265+ ) } `
266+ )
267+ }
268+
269+ const outputPath = path . join ( outputDir , outputFileName )
270+
271+ // Store result
272+ job . result = {
273+ outputPath : outputPath ,
274+ filename : outputFileName ,
275+ }
276+
277+ console . log ( `Export completed: ${ job . id } -> ${ outputPath } ` )
278+ resolve ( )
279+ } catch ( error ) {
280+ reject ( error )
281+ }
282+ } )
283+ } catch ( error ) {
284+ console . error ( `Export failed for job ${ job . id } :` , error )
285+ reject ( error )
286+ }
120287 } )
121288 }
122289
0 commit comments