jtcsv 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +275 -317
  2. package/bin/jtcsv.js +171 -89
  3. package/cli-tui.js +0 -0
  4. package/csv-to-json.js +135 -22
  5. package/dist/jtcsv.cjs.js +1619 -0
  6. package/dist/jtcsv.cjs.js.map +1 -0
  7. package/dist/jtcsv.esm.js +1599 -0
  8. package/dist/jtcsv.esm.js.map +1 -0
  9. package/dist/jtcsv.umd.js +1625 -0
  10. package/dist/jtcsv.umd.js.map +1 -0
  11. package/examples/cli-tool.js +186 -0
  12. package/examples/express-api.js +167 -0
  13. package/examples/large-dataset-example.js +185 -0
  14. package/examples/plugin-excel-exporter.js +407 -0
  15. package/examples/simple-usage.js +280 -0
  16. package/examples/streaming-example.js +419 -0
  17. package/index.d.ts +22 -3
  18. package/index.js +1 -0
  19. package/json-save.js +1 -1
  20. package/json-to-csv.js +16 -6
  21. package/package.json +130 -16
  22. package/plugins/README.md +373 -0
  23. package/plugins/express-middleware/README.md +306 -0
  24. package/plugins/express-middleware/example.js +136 -0
  25. package/plugins/express-middleware/index.d.ts +114 -0
  26. package/plugins/express-middleware/index.js +360 -0
  27. package/plugins/express-middleware/package.json +52 -0
  28. package/plugins/fastify-plugin/index.js +406 -0
  29. package/plugins/fastify-plugin/package.json +55 -0
  30. package/plugins/nextjs-api/README.md +452 -0
  31. package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
  32. package/plugins/nextjs-api/examples/api-convert.js +69 -0
  33. package/plugins/nextjs-api/index.js +388 -0
  34. package/plugins/nextjs-api/package.json +63 -0
  35. package/plugins/nextjs-api/route.js +372 -0
  36. package/src/browser/browser-functions.js +189 -0
  37. package/src/browser/csv-to-json-browser.js +442 -0
  38. package/src/browser/errors-browser.js +194 -0
  39. package/src/browser/index.js +79 -0
  40. package/src/browser/json-to-csv-browser.js +309 -0
  41. package/src/browser/workers/csv-parser.worker.js +359 -0
  42. package/src/browser/workers/worker-pool.js +467 -0
  43. package/src/core/plugin-system.js +472 -0
  44. package/src/engines/fast-path-engine-new.js +338 -0
  45. package/src/engines/fast-path-engine.js +347 -0
  46. package/src/formats/ndjson-parser.js +419 -0
  47. package/src/index-with-plugins.js +349 -0
  48. package/stream-csv-to-json.js +1 -1
  49. package/stream-json-to-csv.js +1 -1
@@ -0,0 +1,467 @@
1
+ // Worker Pool для параллельной обработки CSV
2
+ // Использует Comlink для простой коммуникации с Web Workers
3
+
4
+ import { ValidationError, ConfigurationError } from '../errors-browser.js';
5
+
6
+ // Проверка поддержки Web Workers
7
+ const WORKERS_SUPPORTED = typeof Worker !== 'undefined';
8
+
9
+ /**
10
+ * Опции для Worker Pool
11
+ * @typedef {Object} WorkerPoolOptions
12
+ * @property {number} [workerCount=4] - Количество workers в pool
13
+ * @property {number} [maxQueueSize=100] - Максимальный размер очереди задач
14
+ * @property {boolean} [autoScale=true] - Автоматическое масштабирование pool
15
+ * @property {number} [idleTimeout=60000] - Таймаут простоя worker (мс)
16
+ */
17
+
18
+ /**
19
+ * Статистика Worker Pool
20
+ * @typedef {Object} WorkerPoolStats
21
+ * @property {number} totalWorkers - Всего workers
22
+ * @property {number} activeWorkers - Активные workers
23
+ * @property {number} idleWorkers - Простаивающие workers
24
+ * @property {number} queueSize - Размер очереди
25
+ * @property {number} tasksCompleted - Завершенные задачи
26
+ * @property {number} tasksFailed - Неудачные задачи
27
+ */
28
+
29
+ /**
30
+ * Прогресс обработки задачи
31
+ * @typedef {Object} TaskProgress
32
+ * @property {number} processed - Обработано элементов
33
+ * @property {number} total - Всего элементов
34
+ * @property {number} percentage - Процент выполнения
35
+ * @property {number} speed - Скорость обработки (элементов/сек)
36
+ */
37
+
38
+ /**
39
+ * Worker Pool для параллельной обработки CSV
40
+ */
41
+ export class WorkerPool {
42
+ /**
43
+ * Создает новый Worker Pool
44
+ * @param {string} workerScript - URL скрипта worker
45
+ * @param {WorkerPoolOptions} [options] - Опции pool
46
+ */
47
+ constructor(workerScript, options = {}) {
48
+ if (!WORKERS_SUPPORTED) {
49
+ throw new ValidationError('Web Workers не поддерживаются в этом браузере');
50
+ }
51
+
52
+ this.workerScript = workerScript;
53
+ this.options = {
54
+ workerCount: 4,
55
+ maxQueueSize: 100,
56
+ autoScale: true,
57
+ idleTimeout: 60000,
58
+ ...options
59
+ };
60
+
61
+ this.workers = [];
62
+ this.taskQueue = [];
63
+ this.activeTasks = new Map();
64
+ this.stats = {
65
+ totalWorkers: 0,
66
+ activeWorkers: 0,
67
+ idleWorkers: 0,
68
+ queueSize: 0,
69
+ tasksCompleted: 0,
70
+ tasksFailed: 0
71
+ };
72
+
73
+ this.initializeWorkers();
74
+ }
75
+
76
+ /**
77
+ * Инициализация workers
78
+ * @private
79
+ */
80
+ initializeWorkers() {
81
+ const { workerCount } = this.options;
82
+
83
+ for (let i = 0; i < workerCount; i++) {
84
+ this.createWorker();
85
+ }
86
+
87
+ this.updateStats();
88
+ }
89
+
90
+ /**
91
+ * Создает нового worker
92
+ * @private
93
+ */
94
+ createWorker() {
95
+ try {
96
+ const worker = new Worker(this.workerScript, { type: 'module' });
97
+
98
+ worker.id = `worker-${this.workers.length}`;
99
+ worker.status = 'idle';
100
+ worker.lastUsed = Date.now();
101
+ worker.taskId = null;
102
+
103
+ // Обработчики событий
104
+ worker.onmessage = (event) => this.handleWorkerMessage(worker, event);
105
+ worker.onerror = (error) => this.handleWorkerError(worker, error);
106
+ worker.onmessageerror = (error) => this.handleWorkerMessageError(worker, error);
107
+
108
+ this.workers.push(worker);
109
+ this.stats.totalWorkers++;
110
+ this.stats.idleWorkers++;
111
+
112
+ return worker;
113
+ } catch (error) {
114
+ throw new ConfigurationError(`Не удалось создать worker: ${error.message}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Обработка сообщений от worker
120
+ * @private
121
+ */
122
+ handleWorkerMessage(worker, event) {
123
+ const { data } = event;
124
+
125
+ if (data.type === 'PROGRESS') {
126
+ this.handleProgress(worker, data);
127
+ } else if (data.type === 'RESULT') {
128
+ this.handleResult(worker, data);
129
+ } else if (data.type === 'ERROR') {
130
+ this.handleWorkerTaskError(worker, data);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Обработка прогресса задачи
136
+ * @private
137
+ */
138
+ handleProgress(worker, progressData) {
139
+ const taskId = worker.taskId;
140
+ if (taskId && this.activeTasks.has(taskId)) {
141
+ const task = this.activeTasks.get(taskId);
142
+ if (task.onProgress) {
143
+ task.onProgress({
144
+ processed: progressData.processed,
145
+ total: progressData.total,
146
+ percentage: (progressData.processed / progressData.total) * 100,
147
+ speed: progressData.speed || 0
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Обработка результата задачи
155
+ * @private
156
+ */
157
+ handleResult(worker, resultData) {
158
+ const taskId = worker.taskId;
159
+ if (taskId && this.activeTasks.has(taskId)) {
160
+ const task = this.activeTasks.get(taskId);
161
+
162
+ // Освобождение worker
163
+ worker.status = 'idle';
164
+ worker.lastUsed = Date.now();
165
+ worker.taskId = null;
166
+ this.stats.activeWorkers--;
167
+ this.stats.idleWorkers++;
168
+
169
+ // Завершение задачи
170
+ task.resolve(resultData.data);
171
+ this.activeTasks.delete(taskId);
172
+ this.stats.tasksCompleted++;
173
+
174
+ // Обработка следующей задачи в очереди
175
+ this.processQueue();
176
+ this.updateStats();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Обработка ошибки задачи
182
+ * @private
183
+ */
184
+ handleWorkerTaskError(worker, errorData) {
185
+ const taskId = worker.taskId;
186
+ if (taskId && this.activeTasks.has(taskId)) {
187
+ const task = this.activeTasks.get(taskId);
188
+
189
+ // Освобождение worker
190
+ worker.status = 'idle';
191
+ worker.lastUsed = Date.now();
192
+ worker.taskId = null;
193
+ this.stats.activeWorkers--;
194
+ this.stats.idleWorkers++;
195
+
196
+ // Завершение с ошибкой
197
+ task.reject(new Error(errorData.message || 'Ошибка в worker'));
198
+ this.activeTasks.delete(taskId);
199
+ this.stats.tasksFailed++;
200
+
201
+ // Обработка следующей задачи
202
+ this.processQueue();
203
+ this.updateStats();
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Обработка ошибок worker
209
+ * @private
210
+ */
211
+ handleWorkerError(worker, error) {
212
+ console.error(`Worker ${worker.id} error:`, error);
213
+
214
+ // Перезапуск worker
215
+ this.restartWorker(worker);
216
+ }
217
+
218
+ /**
219
+ * Обработка ошибок сообщений
220
+ * @private
221
+ */
222
+ handleWorkerMessageError(worker, error) {
223
+ console.error(`Worker ${worker.id} message error:`, error);
224
+ }
225
+
226
+ /**
227
+ * Перезапуск worker
228
+ * @private
229
+ */
230
+ restartWorker(worker) {
231
+ const index = this.workers.indexOf(worker);
232
+ if (index !== -1) {
233
+ // Завершение старого worker
234
+ worker.terminate();
235
+
236
+ // Удаление из статистики
237
+ if (worker.status === 'active') {
238
+ this.stats.activeWorkers--;
239
+ } else {
240
+ this.stats.idleWorkers--;
241
+ }
242
+ this.stats.totalWorkers--;
243
+
244
+ // Создание нового worker
245
+ const newWorker = this.createWorker();
246
+ this.workers[index] = newWorker;
247
+
248
+ // Перезапуск задачи если была активна
249
+ if (worker.taskId && this.activeTasks.has(worker.taskId)) {
250
+ const task = this.activeTasks.get(worker.taskId);
251
+ this.executeTask(newWorker, task);
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Выполнение задачи на worker
258
+ * @private
259
+ */
260
+ executeTask(worker, task) {
261
+ worker.status = 'active';
262
+ worker.lastUsed = Date.now();
263
+ worker.taskId = task.id;
264
+
265
+ this.stats.idleWorkers--;
266
+ this.stats.activeWorkers++;
267
+
268
+ // Отправка задачи в worker
269
+ worker.postMessage({
270
+ type: 'EXECUTE',
271
+ taskId: task.id,
272
+ method: task.method,
273
+ args: task.args,
274
+ options: task.options
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Обработка очереди задач
280
+ * @private
281
+ */
282
+ processQueue() {
283
+ if (this.taskQueue.length === 0) {
284
+ return;
285
+ }
286
+
287
+ // Поиск свободного worker
288
+ const idleWorker = this.workers.find(w => w.status === 'idle');
289
+ if (!idleWorker) {
290
+ // Автомасштабирование если включено
291
+ if (this.options.autoScale && this.workers.length < this.options.maxQueueSize) {
292
+ this.createWorker();
293
+ this.processQueue();
294
+ }
295
+ return;
296
+ }
297
+
298
+ // Получение задачи из очереди
299
+ const task = this.taskQueue.shift();
300
+ this.stats.queueSize--;
301
+
302
+ // Выполнение задачи
303
+ this.executeTask(idleWorker, task);
304
+ this.updateStats();
305
+ }
306
+
307
+ /**
308
+ * Обновление статистики
309
+ * @private
310
+ */
311
+ updateStats() {
312
+ this.stats.queueSize = this.taskQueue.length;
313
+ }
314
+
315
+ /**
316
+ * Выполнение задачи через pool
317
+ * @param {string} method - Метод для вызова в worker
318
+ * @param {Array} args - Аргументы метода
319
+ * @param {Object} [options] - Опции задачи
320
+ * @param {Function} [onProgress] - Callback прогресса
321
+ * @returns {Promise<any>} Результат выполнения
322
+ */
323
+ async exec(method, args = [], options = {}, onProgress = null) {
324
+ return new Promise((resolve, reject) => {
325
+ // Проверка размера очереди
326
+ if (this.taskQueue.length >= this.options.maxQueueSize) {
327
+ reject(new Error('Очередь задач переполнена'));
328
+ return;
329
+ }
330
+
331
+ // Создание задачи
332
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
333
+ const task = {
334
+ id: taskId,
335
+ method,
336
+ args,
337
+ options,
338
+ onProgress,
339
+ resolve,
340
+ reject,
341
+ createdAt: Date.now()
342
+ };
343
+
344
+ // Добавление в очередь
345
+ this.taskQueue.push(task);
346
+ this.stats.queueSize++;
347
+
348
+ // Запуск обработки очереди
349
+ this.processQueue();
350
+ this.updateStats();
351
+ });
352
+ }
353
+
354
+ /**
355
+ * Получение статистики pool
356
+ * @returns {WorkerPoolStats} Статистика
357
+ */
358
+ getStats() {
359
+ return { ...this.stats };
360
+ }
361
+
362
+ /**
363
+ * Очистка простаивающих workers
364
+ */
365
+ cleanupIdleWorkers() {
366
+ const now = Date.now();
367
+ const { idleTimeout } = this.options;
368
+
369
+ for (let i = this.workers.length - 1; i >= 0; i--) {
370
+ const worker = this.workers[i];
371
+ if (worker.status === 'idle' && (now - worker.lastUsed) > idleTimeout) {
372
+ // Сохранение минимального количества workers
373
+ if (this.workers.length > 1) {
374
+ worker.terminate();
375
+ this.workers.splice(i, 1);
376
+ this.stats.totalWorkers--;
377
+ this.stats.idleWorkers--;
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Завершение всех workers
385
+ */
386
+ terminate() {
387
+ this.workers.forEach(worker => {
388
+ worker.terminate();
389
+ });
390
+
391
+ this.workers = [];
392
+ this.taskQueue = [];
393
+ this.activeTasks.clear();
394
+
395
+ // Сброс статистики
396
+ this.stats = {
397
+ totalWorkers: 0,
398
+ activeWorkers: 0,
399
+ idleWorkers: 0,
400
+ queueSize: 0,
401
+ tasksCompleted: 0,
402
+ tasksFailed: 0
403
+ };
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Создает Worker Pool для обработки CSV
409
+ * @param {WorkerPoolOptions} [options] - Опции pool
410
+ * @returns {WorkerPool} Worker Pool
411
+ */
412
+ export function createWorkerPool(options = {}) {
413
+ // Используем встроенный worker скрипт
414
+ const workerScript = new URL('./csv-parser.worker.js', import.meta.url).href;
415
+ return new WorkerPool(workerScript, options);
416
+ }
417
+
418
+ /**
419
+ * Парсит CSV с использованием Web Workers
420
+ * @param {string|File} csvInput - CSV строка или File объект
421
+ * @param {Object} [options] - Опции парсинга
422
+ * @param {Function} [onProgress] - Callback прогресса
423
+ * @returns {Promise<Array<Object>>} JSON данные
424
+ */
425
+ export async function parseCSVWithWorker(csvInput, options = {}, onProgress = null) {
426
+ // Создание pool если нужно
427
+ if (!parseCSVWithWorker.pool) {
428
+ parseCSVWithWorker.pool = createWorkerPool();
429
+ }
430
+
431
+ const pool = parseCSVWithWorker.pool;
432
+
433
+ // Подготовка CSV строки
434
+ let csvString;
435
+ if (csvInput instanceof File) {
436
+ csvString = await readFileAsText(csvInput);
437
+ } else if (typeof csvInput === 'string') {
438
+ csvString = csvInput;
439
+ } else {
440
+ throw new ValidationError('Input must be a CSV string or File object');
441
+ }
442
+
443
+ // Выполнение через pool
444
+ return pool.exec('parseCSV', [csvString, options], {}, onProgress);
445
+ }
446
+
447
+ /**
448
+ * Чтение файла как текст
449
+ * @private
450
+ */
451
+ async function readFileAsText(file) {
452
+ return new Promise((resolve, reject) => {
453
+ const reader = new FileReader();
454
+ reader.onload = (event) => resolve(event.target.result);
455
+ reader.onerror = (error) => reject(error);
456
+ reader.readAsText(file, 'UTF-8');
457
+ });
458
+ }
459
+
460
+ // Экспорт для Node.js совместимости
461
+ if (typeof module !== 'undefined' && module.exports) {
462
+ module.exports = {
463
+ WorkerPool,
464
+ createWorkerPool,
465
+ parseCSVWithWorker
466
+ };
467
+ }