/
grigoriygrisha
/
excel-loader
Обзор
Документация
Войти
/
grigoriygrisha
/
excel-loader
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/core/ExcelLoader.ts
228 строк
6 KB
grigoriy-grisha
first_commit
26 июл 2026, 21:46
26 июл 2026, 21:46
6cc93a5
Код
Авторство
О чём код?
import ParserWorker from '../worker/parser.worker.ts?worker&inline' import { repository } from '../db/repository.js' import { FormatDetector } from '../utils/detectFormat.js' import { formatBytes } from '../utils/format.js' import { StorageGuard } from '../utils/storage.js' import { FileTooLargeError, type ColumnMeta, type CellValue, type FileFormat, type LoadResult, type LoaderOptions, } from './types.js' import type { DoneMessage, ErrorMessage, ColumnsMessage, } from '../worker/protocol.js' const DEFAULT_BATCH_SIZE = 5000 const DEFAULT_MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 export class ExcelLoader { private readonly options: Required< Omit<LoaderOptions, 'csvDelimiter'> > & { csvDelimiter?: string } private readonly formatDetector = new FormatDetector() private readonly storageGuard = new StorageGuard() private worker: Worker | null = null private datasetId: string | null = null private columns: ColumnMeta[] = [] private totalRows = 0 constructor(options: LoaderOptions = {}) { this.options = { batchSize: options.batchSize ?? DEFAULT_BATCH_SIZE, hasHeader: options.hasHeader ?? true, maxFileSize: options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE, csvDelimiter: options.csvDelimiter, } } async loadFile(file: File, hint?: FileFormat): Promise<LoadResult> { if (file.size > this.options.maxFileSize) { throw new FileTooLargeError( `File is ${formatBytes(file.size)} which exceeds the configured limit ` + `of ${formatBytes(this.options.maxFileSize)}.`, file.size, this.options.maxFileSize, ) } await this.storageGuard.assertAvailable(file.size) void this.storageGuard.requestPersistent() const format = await this.formatDetector.detect(file, hint) if (this.datasetId) { await repository.clearDataset(this.datasetId).catch(() => {}) } this.columns = [] this.totalRows = 0 const datasetId = this.makeDatasetId() this.datasetId = datasetId this.terminateWorker() const worker = new ParserWorker() this.worker = worker return new Promise<LoadResult>((resolve, reject) => { const onMessage = (event: MessageEvent) => { const msg = event.data as | ColumnsMessage | DoneMessage | ErrorMessage switch (msg.type) { case 'COLUMNS': this.columns = msg.columns break case 'DONE': this.totalRows = msg.totalRows cleanup() resolve({ datasetId, format, columns: this.columns, totalRows: msg.totalRows, }) break case 'ERROR': { cleanup() const err = this.reconstructError(msg.name, msg.message) if (msg.stack) (err as Error & { stack?: string }).stack = msg.stack reject(err) break } } } const onError = (event: ErrorEvent) => { cleanup() const err = event.error instanceof Error ? event.error : new Error( `The parser worker crashed: ${event.message || 'unknown error'} ` + `(at ${event.filename ?? '?'}:${event.lineno ?? '?'}:${event.colno ?? '?'})`, ) console.error('[excel-load] worker.onerror:', err, event) reject(err) } const onMessageError = (event: MessageEvent) => { cleanup() console.error('[excel-load] worker.messageerror:', event) reject(new Error('Failed to deserialize a message from the parser worker.')) } const cleanup = () => { worker.removeEventListener('message', onMessage) worker.removeEventListener('error', onError) worker.removeEventListener('messageerror', onMessageError) } worker.addEventListener('message', onMessage) worker.addEventListener('error', onError) worker.addEventListener('messageerror', onMessageError) worker.postMessage({ type: 'LOAD', payload: { file, datasetId, format, batchSize: this.options.batchSize, hasHeader: this.options.hasHeader, csvDelimiter: this.options.csvDelimiter, }, }) }) } async abort(): Promise<void> { if (this.worker) { this.worker.postMessage({ type: 'ABORT' }) } if (this.datasetId) { await repository.clearDataset(this.datasetId).catch(() => {}) } this.resetState() } getColumns(): ColumnMeta[] { return this.columns } getTotalRows(): number { return this.totalRows } async getRows(start: number, end: number): Promise<CellValue[][]> { if (!this.datasetId) return [] if (end <= start) return [] const records = await repository.getRange(this.datasetId, start, end) return records.map((r) => r.values) } async clear(): Promise<void> { if (this.datasetId) { await repository.clearDataset(this.datasetId).catch(() => {}) } this.terminateWorker() this.resetState() } dispose(): void { this.terminateWorker() this.resetState() } private terminateWorker(): void { if (this.worker) { this.worker.terminate() this.worker = null } } private resetState(): void { this.datasetId = null this.columns = [] this.totalRows = 0 } private makeDatasetId(): string { if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { return crypto.randomUUID() } return `ds_${Date.now()}_${Math.random().toString(36).slice(2)}` } private reconstructError(name: string, message: string): Error { switch (name) { case 'FileTooLargeError': { const e = new Error(message) e.name = 'FileTooLargeError' return e } case 'StorageQuotaError': { const e = new Error(message) e.name = 'StorageQuotaError' return e } case 'UnsupportedFormatError': { const e = new Error(message) e.name = 'UnsupportedFormatError' return e } default: return new Error(message) } } } export function createExcelLoader(options?: LoaderOptions): ExcelLoader { return new ExcelLoader(options) }