@flowlab/all
Version:
A cool library focusing on handling various flows
77 lines (70 loc) • 3.58 kB
text/typescript
// src/loaders/fileLoader.ts
import * as fs from 'fs/promises';
import { ILoader, PipelineContext, FileTargetConfig } from '../core/interfaces';
import { ComponentError } from '../core/errors';
import { EOL } from 'os'; // End Of Line constant
// MARK: - FileLoader
export class FileLoader<TInput extends (string | object)> implements ILoader<TInput> {
private config: FileTargetConfig;
private writeStream: fs.FileHandle | null = null; // Keep handle open for append mode
constructor(config: FileTargetConfig) {
if (!config.path || !config.format) {
throw new ComponentError('FileLoader requires "path" and "format" in config.');
}
this.config = config;
this.config.mode = config.mode || 'overwrite';
}
// MARK: - initStream
private async initializeStream(context: PipelineContext): Promise<void> {
if (!this.writeStream) {
const flags = this.config.mode === 'append' ? 'a' : 'w';
context.logger.debug(`Opening file <span class="math-inline">\{this\.config\.path\} in mode '</span>{flags}'`);
try {
this.writeStream = await fs.open(this.config.path, flags);
} catch (error: any) {
throw new ComponentError(`Failed to open file ${this.config.path} for writing`, 'FileLoader', error);
}
}
}
// MARK: - loadBatch
async loadBatch(batch: TInput[], context: PipelineContext): Promise<void> {
if (batch.length === 0) return;
await this.initializeStream(context); // Ensure stream is open
let contentToWrite = '';
for (const item of batch) {
if (this.config.format === 'json') {
// Write JSON Lines format
contentToWrite += JSON.stringify(item) + EOL;
} else if (this.config.format === 'csv') {
// Basic CSV - join array elements, or stringify object values
// Consider a proper CSV library ('csv-stringify') for complex objects/quoting
const line = Array.isArray(item) ? item.join(',') : typeof item === 'object' ? Object.values(item).join(',') : String(item);
contentToWrite += line + EOL;
} else { // text format
contentToWrite += String(item) + EOL;
}
}
try {
await this.writeStream!.writeFile(contentToWrite, { encoding: this.config.encoding || 'utf-8' });
context.logger.trace(`Wrote batch of ${batch.length} items to ${this.config.path}`);
} catch (error: any) {
context.logger.error({ err: error, file: this.config.path }, `Error writing batch to file`);
throw new ComponentError(`Error writing to file ${this.config.path}`, 'FileLoader', error);
}
// Note: We keep the stream open in append mode until explicitly closed or pipeline ends.
// Need a mechanism to close it (e.g., a `shutdown` method on the loader called by the pipeline).
}
// MARK: - shutdown
// Optional: Add a shutdown method to close the file handle
async shutdown(context: PipelineContext): Promise<void> {
if (this.writeStream) {
context.logger.debug(`Closing file handle for ${this.config.path}`);
try {
await this.writeStream.close();
this.writeStream = null;
} catch (error: any) {
context.logger.error({ err: error, file: this.config.path }, `Error closing file handle`);
}
}
}
}