jtcsv 1.2.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 (46) hide show
  1. package/README.md +252 -337
  2. package/bin/jtcsv.js +167 -85
  3. package/cli-tui.js +0 -0
  4. package/dist/jtcsv.cjs.js +1619 -0
  5. package/dist/jtcsv.cjs.js.map +1 -0
  6. package/dist/jtcsv.esm.js +1599 -0
  7. package/dist/jtcsv.esm.js.map +1 -0
  8. package/dist/jtcsv.umd.js +1625 -0
  9. package/dist/jtcsv.umd.js.map +1 -0
  10. package/examples/cli-tool.js +186 -0
  11. package/examples/express-api.js +167 -0
  12. package/examples/large-dataset-example.js +185 -0
  13. package/examples/plugin-excel-exporter.js +407 -0
  14. package/examples/simple-usage.js +280 -0
  15. package/examples/streaming-example.js +419 -0
  16. package/index.d.ts +4 -0
  17. package/json-save.js +1 -1
  18. package/package.json +128 -14
  19. package/plugins/README.md +373 -0
  20. package/plugins/express-middleware/README.md +306 -0
  21. package/plugins/express-middleware/example.js +136 -0
  22. package/plugins/express-middleware/index.d.ts +114 -0
  23. package/plugins/express-middleware/index.js +360 -0
  24. package/plugins/express-middleware/package.json +52 -0
  25. package/plugins/fastify-plugin/index.js +406 -0
  26. package/plugins/fastify-plugin/package.json +55 -0
  27. package/plugins/nextjs-api/README.md +452 -0
  28. package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
  29. package/plugins/nextjs-api/examples/api-convert.js +69 -0
  30. package/plugins/nextjs-api/index.js +388 -0
  31. package/plugins/nextjs-api/package.json +63 -0
  32. package/plugins/nextjs-api/route.js +372 -0
  33. package/src/browser/browser-functions.js +189 -0
  34. package/src/browser/csv-to-json-browser.js +442 -0
  35. package/src/browser/errors-browser.js +194 -0
  36. package/src/browser/index.js +79 -0
  37. package/src/browser/json-to-csv-browser.js +309 -0
  38. package/src/browser/workers/csv-parser.worker.js +359 -0
  39. package/src/browser/workers/worker-pool.js +467 -0
  40. package/src/core/plugin-system.js +472 -0
  41. package/src/engines/fast-path-engine-new.js +338 -0
  42. package/src/engines/fast-path-engine.js +347 -0
  43. package/src/formats/ndjson-parser.js +419 -0
  44. package/src/index-with-plugins.js +349 -0
  45. package/stream-csv-to-json.js +1 -1
  46. package/stream-json-to-csv.js +1 -1
@@ -0,0 +1,1619 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
+ // Система ошибок для браузерной версии jtcsv
7
+ // Адаптирована для работы без Node.js специфичных API
8
+
9
+ /**
10
+ * Базовый класс ошибки jtcsv
11
+ */
12
+ class JTCSVError extends Error {
13
+ constructor(message, code = 'JTCSV_ERROR', details = {}) {
14
+ super(message);
15
+ this.name = 'JTCSVError';
16
+ this.code = code;
17
+ this.details = details;
18
+
19
+ // Сохранение stack trace
20
+ if (Error.captureStackTrace) {
21
+ Error.captureStackTrace(this, JTCSVError);
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Ошибка валидации
28
+ */
29
+ class ValidationError extends JTCSVError {
30
+ constructor(message, details = {}) {
31
+ super(message, 'VALIDATION_ERROR', details);
32
+ this.name = 'ValidationError';
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Ошибка безопасности
38
+ */
39
+ class SecurityError extends JTCSVError {
40
+ constructor(message, details = {}) {
41
+ super(message, 'SECURITY_ERROR', details);
42
+ this.name = 'SecurityError';
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Ошибка файловой системы (адаптирована для браузера)
48
+ */
49
+ class FileSystemError extends JTCSVError {
50
+ constructor(message, originalError = null, details = {}) {
51
+ super(message, 'FILE_SYSTEM_ERROR', {
52
+ ...details,
53
+ originalError
54
+ });
55
+ this.name = 'FileSystemError';
56
+ if (originalError && originalError.code) {
57
+ this.code = originalError.code;
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Ошибка парсинга
64
+ */
65
+ class ParsingError extends JTCSVError {
66
+ constructor(message, lineNumber = null, details = {}) {
67
+ super(message, 'PARSING_ERROR', {
68
+ ...details,
69
+ lineNumber
70
+ });
71
+ this.name = 'ParsingError';
72
+ this.lineNumber = lineNumber;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ошибка превышения лимита
78
+ */
79
+ class LimitError extends JTCSVError {
80
+ constructor(message, limit, actual, details = {}) {
81
+ super(message, 'LIMIT_ERROR', {
82
+ ...details,
83
+ limit,
84
+ actual
85
+ });
86
+ this.name = 'LimitError';
87
+ this.limit = limit;
88
+ this.actual = actual;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Ошибка конфигурации
94
+ */
95
+ class ConfigurationError extends JTCSVError {
96
+ constructor(message, details = {}) {
97
+ super(message, 'CONFIGURATION_ERROR', details);
98
+ this.name = 'ConfigurationError';
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Безопасное выполнение функции с обработкой ошибок
104
+ *
105
+ * @param {Function} fn - Функция для выполнения
106
+ * @param {string} errorCode - Код ошибки по умолчанию
107
+ * @param {Object} errorDetails - Детали ошибки
108
+ * @returns {*} Результат выполнения функции
109
+ */
110
+ function safeExecute(fn, errorCode = 'UNKNOWN_ERROR', errorDetails = {}) {
111
+ try {
112
+ if (typeof fn === 'function') {
113
+ return fn();
114
+ }
115
+ throw new ValidationError('Function expected');
116
+ } catch (error) {
117
+ // Если ошибка уже является JTCSVError, перебросить её
118
+ if (error instanceof JTCSVError) {
119
+ throw error;
120
+ }
121
+
122
+ // Определить тип ошибки на основе сообщения или кода
123
+ let enhancedError;
124
+ const errorMessage = error.message || String(error);
125
+ if (errorMessage.includes('validation') || errorMessage.includes('Validation')) {
126
+ enhancedError = new ValidationError(errorMessage, {
127
+ ...errorDetails,
128
+ originalError: error
129
+ });
130
+ } else if (errorMessage.includes('security') || errorMessage.includes('Security')) {
131
+ enhancedError = new SecurityError(errorMessage, {
132
+ ...errorDetails,
133
+ originalError: error
134
+ });
135
+ } else if (errorMessage.includes('parsing') || errorMessage.includes('Parsing')) {
136
+ enhancedError = new ParsingError(errorMessage, null, {
137
+ ...errorDetails,
138
+ originalError: error
139
+ });
140
+ } else if (errorMessage.includes('limit') || errorMessage.includes('Limit')) {
141
+ enhancedError = new LimitError(errorMessage, null, null, {
142
+ ...errorDetails,
143
+ originalError: error
144
+ });
145
+ } else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
146
+ enhancedError = new ConfigurationError(errorMessage, {
147
+ ...errorDetails,
148
+ originalError: error
149
+ });
150
+ } else if (errorMessage.includes('file') || errorMessage.includes('File')) {
151
+ enhancedError = new FileSystemError(errorMessage, error, errorDetails);
152
+ } else {
153
+ // Общая ошибка
154
+ enhancedError = new JTCSVError(errorMessage, errorCode, {
155
+ ...errorDetails,
156
+ originalError: error
157
+ });
158
+ }
159
+
160
+ // Сохранить оригинальный stack trace если возможно
161
+ if (error.stack) {
162
+ enhancedError.stack = error.stack;
163
+ }
164
+ throw enhancedError;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Асинхронная версия safeExecute
170
+ */
171
+ async function safeExecuteAsync(fn, errorCode = 'UNKNOWN_ERROR', errorDetails = {}) {
172
+ try {
173
+ if (typeof fn === 'function') {
174
+ return await fn();
175
+ }
176
+ throw new ValidationError('Function expected');
177
+ } catch (error) {
178
+ // Если ошибка уже является JTCSVError, перебросить её
179
+ if (error instanceof JTCSVError) {
180
+ throw error;
181
+ }
182
+
183
+ // Определить тип ошибки
184
+ let enhancedError;
185
+ const errorMessage = error.message || String(error);
186
+ if (errorMessage.includes('validation') || errorMessage.includes('Validation')) {
187
+ enhancedError = new ValidationError(errorMessage, {
188
+ ...errorDetails,
189
+ originalError: error
190
+ });
191
+ } else if (errorMessage.includes('security') || errorMessage.includes('Security')) {
192
+ enhancedError = new SecurityError(errorMessage, {
193
+ ...errorDetails,
194
+ originalError: error
195
+ });
196
+ } else if (errorMessage.includes('parsing') || errorMessage.includes('Parsing')) {
197
+ enhancedError = new ParsingError(errorMessage, null, {
198
+ ...errorDetails,
199
+ originalError: error
200
+ });
201
+ } else if (errorMessage.includes('limit') || errorMessage.includes('Limit')) {
202
+ enhancedError = new LimitError(errorMessage, null, null, {
203
+ ...errorDetails,
204
+ originalError: error
205
+ });
206
+ } else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
207
+ enhancedError = new ConfigurationError(errorMessage, {
208
+ ...errorDetails,
209
+ originalError: error
210
+ });
211
+ } else if (errorMessage.includes('file') || errorMessage.includes('File')) {
212
+ enhancedError = new FileSystemError(errorMessage, error, errorDetails);
213
+ } else {
214
+ enhancedError = new JTCSVError(errorMessage, errorCode, {
215
+ ...errorDetails,
216
+ originalError: error
217
+ });
218
+ }
219
+ if (error.stack) {
220
+ enhancedError.stack = error.stack;
221
+ }
222
+ throw enhancedError;
223
+ }
224
+ }
225
+
226
+ // Экспорт для Node.js совместимости
227
+ if (typeof module !== 'undefined' && module.exports) {
228
+ module.exports = {
229
+ JTCSVError,
230
+ ValidationError,
231
+ SecurityError,
232
+ FileSystemError,
233
+ ParsingError,
234
+ LimitError,
235
+ ConfigurationError,
236
+ safeExecute,
237
+ safeExecuteAsync
238
+ };
239
+ }
240
+
241
+ // Браузерная версия JSON to CSV конвертера
242
+ // Адаптирована для работы в браузере без Node.js API
243
+
244
+
245
+ /**
246
+ * Валидация входных данных и опций
247
+ * @private
248
+ */
249
+ function validateInput(data, options) {
250
+ // Validate data
251
+ if (!Array.isArray(data)) {
252
+ throw new ValidationError('Input data must be an array');
253
+ }
254
+
255
+ // Validate options
256
+ if (options && typeof options !== 'object') {
257
+ throw new ConfigurationError('Options must be an object');
258
+ }
259
+
260
+ // Validate delimiter
261
+ if (options !== null && options !== void 0 && options.delimiter && typeof options.delimiter !== 'string') {
262
+ throw new ConfigurationError('Delimiter must be a string');
263
+ }
264
+ if (options !== null && options !== void 0 && options.delimiter && options.delimiter.length !== 1) {
265
+ throw new ConfigurationError('Delimiter must be a single character');
266
+ }
267
+
268
+ // Validate renameMap
269
+ if (options !== null && options !== void 0 && options.renameMap && typeof options.renameMap !== 'object') {
270
+ throw new ConfigurationError('renameMap must be an object');
271
+ }
272
+
273
+ // Validate maxRecords
274
+ if (options && options.maxRecords !== undefined) {
275
+ if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
276
+ throw new ConfigurationError('maxRecords must be a positive number');
277
+ }
278
+ }
279
+
280
+ // Validate preventCsvInjection
281
+ if ((options === null || options === void 0 ? void 0 : options.preventCsvInjection) !== undefined && typeof options.preventCsvInjection !== 'boolean') {
282
+ throw new ConfigurationError('preventCsvInjection must be a boolean');
283
+ }
284
+
285
+ // Validate rfc4180Compliant
286
+ if ((options === null || options === void 0 ? void 0 : options.rfc4180Compliant) !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
287
+ throw new ConfigurationError('rfc4180Compliant must be a boolean');
288
+ }
289
+ return true;
290
+ }
291
+
292
+ /**
293
+ * Конвертирует JSON данные в CSV формат
294
+ *
295
+ * @param {Array<Object>} data - Массив объектов для конвертации в CSV
296
+ * @param {Object} [options] - Опции конфигурации
297
+ * @param {string} [options.delimiter=';'] - CSV разделитель
298
+ * @param {boolean} [options.includeHeaders=true] - Включать ли заголовки
299
+ * @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
300
+ * @param {Object} [options.template={}] - Шаблон для порядка колонок
301
+ * @param {number} [options.maxRecords] - Максимальное количество записей
302
+ * @param {boolean} [options.preventCsvInjection=true] - Защита от CSV инъекций
303
+ * @param {boolean} [options.rfc4180Compliant=true] - Соответствие RFC 4180
304
+ * @returns {string} CSV строка
305
+ */
306
+ function jsonToCsv(data, options = {}) {
307
+ return safeExecute(() => {
308
+ // Валидация входных данных
309
+ validateInput(data, options);
310
+ const opts = options && typeof options === 'object' ? options : {};
311
+ const {
312
+ delimiter = ';',
313
+ includeHeaders = true,
314
+ renameMap = {},
315
+ template = {},
316
+ maxRecords,
317
+ preventCsvInjection = true,
318
+ rfc4180Compliant = true
319
+ } = opts;
320
+
321
+ // Обработка пустых данных
322
+ if (data.length === 0) {
323
+ return '';
324
+ }
325
+
326
+ // Предупреждение для больших наборов данных
327
+ if (data.length > 1000000 && !maxRecords && process.env.NODE_ENV !== 'production') {
328
+ console.warn('⚠️ Warning: Processing >1M records in memory may be slow.\n' + '💡 Consider processing data in batches or using Web Workers for large files.\n' + '📊 Current size: ' + data.length.toLocaleString() + ' records');
329
+ }
330
+
331
+ // Применение ограничения по количеству записей
332
+ if (maxRecords && data.length > maxRecords) {
333
+ throw new LimitError(`Data size exceeds maximum limit of ${maxRecords} records`, maxRecords, data.length);
334
+ }
335
+
336
+ // Получение всех уникальных ключей
337
+ const allKeys = new Set();
338
+ data.forEach(item => {
339
+ if (!item || typeof item !== 'object') {
340
+ return;
341
+ }
342
+ Object.keys(item).forEach(key => allKeys.add(key));
343
+ });
344
+ const originalKeys = Array.from(allKeys);
345
+
346
+ // Применение rename map для создания заголовков
347
+ const headers = originalKeys.map(key => renameMap[key] || key);
348
+
349
+ // Создание обратного маппинга
350
+ const reverseRenameMap = {};
351
+ originalKeys.forEach((key, index) => {
352
+ reverseRenameMap[headers[index]] = key;
353
+ });
354
+
355
+ // Применение порядка из шаблона
356
+ let finalHeaders = headers;
357
+ if (Object.keys(template).length > 0) {
358
+ const templateHeaders = Object.keys(template).map(key => renameMap[key] || key);
359
+ const extraHeaders = headers.filter(h => !templateHeaders.includes(h));
360
+ finalHeaders = [...templateHeaders, ...extraHeaders];
361
+ }
362
+
363
+ /**
364
+ * Экранирование значения для CSV с защитой от инъекций
365
+ * @private
366
+ */
367
+ const escapeValue = value => {
368
+ if (value === null || value === undefined || value === '') {
369
+ return '';
370
+ }
371
+ const stringValue = String(value);
372
+
373
+ // Защита от CSV инъекций
374
+ let escapedValue = stringValue;
375
+ if (preventCsvInjection && /^[=+\-@]/.test(stringValue)) {
376
+ escapedValue = "'" + stringValue;
377
+ }
378
+
379
+ // Соответствие RFC 4180
380
+ const needsQuoting = rfc4180Compliant ? escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r') : escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r');
381
+ if (needsQuoting) {
382
+ return `"${escapedValue.replace(/"/g, '""')}"`;
383
+ }
384
+ return escapedValue;
385
+ };
386
+
387
+ // Построение CSV строк
388
+ const rows = [];
389
+
390
+ // Добавление заголовков
391
+ if (includeHeaders && finalHeaders.length > 0) {
392
+ rows.push(finalHeaders.join(delimiter));
393
+ }
394
+
395
+ // Добавление данных
396
+ for (const item of data) {
397
+ if (!item || typeof item !== 'object') {
398
+ continue;
399
+ }
400
+ const row = finalHeaders.map(header => {
401
+ const originalKey = reverseRenameMap[header] || header;
402
+ const value = item[originalKey];
403
+ return escapeValue(value);
404
+ }).join(delimiter);
405
+ rows.push(row);
406
+ }
407
+
408
+ // Разделители строк согласно RFC 4180
409
+ const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
410
+ return rows.join(lineEnding);
411
+ }, 'PARSE_FAILED', {
412
+ function: 'jsonToCsv'
413
+ });
414
+ }
415
+
416
+ /**
417
+ * Глубокое разворачивание вложенных объектов и массивов
418
+ *
419
+ * @param {*} value - Значение для разворачивания
420
+ * @param {number} [depth=0] - Текущая глубина рекурсии
421
+ * @param {number} [maxDepth=5] - Максимальная глубина рекурсии
422
+ * @param {Set} [visited=new Set()] - Посещенные объекты для обнаружения циклических ссылок
423
+ * @returns {string} Развернутое строковое значение
424
+ */
425
+ function deepUnwrap(value, depth = 0, maxDepth = 5, visited = new Set()) {
426
+ // Проверка глубины
427
+ if (depth >= maxDepth) {
428
+ return '[Too Deep]';
429
+ }
430
+ if (value === null || value === undefined) {
431
+ return '';
432
+ }
433
+
434
+ // Обработка циклических ссылок
435
+ if (typeof value === 'object') {
436
+ if (visited.has(value)) {
437
+ return '[Circular Reference]';
438
+ }
439
+ visited.add(value);
440
+ }
441
+
442
+ // Обработка массивов
443
+ if (Array.isArray(value)) {
444
+ if (value.length === 0) {
445
+ return '';
446
+ }
447
+ const unwrappedItems = value.map(item => deepUnwrap(item, depth + 1, maxDepth, visited)).filter(item => item !== '');
448
+ return unwrappedItems.join(', ');
449
+ }
450
+
451
+ // Обработка объектов
452
+ if (typeof value === 'object') {
453
+ const keys = Object.keys(value);
454
+ if (keys.length === 0) {
455
+ return '';
456
+ }
457
+ if (depth + 1 >= maxDepth) {
458
+ return '[Too Deep]';
459
+ }
460
+
461
+ // Сериализация сложных объектов
462
+ try {
463
+ return JSON.stringify(value);
464
+ } catch (error) {
465
+ if (error.message.includes('circular') || error.message.includes('Converting circular')) {
466
+ return '[Circular Reference]';
467
+ }
468
+ return '[Unstringifiable Object]';
469
+ }
470
+ }
471
+
472
+ // Примитивные значения
473
+ return String(value);
474
+ }
475
+
476
+ /**
477
+ * Предобработка JSON данных путем глубокого разворачивания вложенных структур
478
+ *
479
+ * @param {Array<Object>} data - Массив объектов для предобработки
480
+ * @returns {Array<Object>} Предобработанные данные с развернутыми значениями
481
+ */
482
+ function preprocessData(data) {
483
+ if (!Array.isArray(data)) {
484
+ return [];
485
+ }
486
+ return data.map(item => {
487
+ if (!item || typeof item !== 'object') {
488
+ return {};
489
+ }
490
+ const processed = {};
491
+ for (const key in item) {
492
+ if (Object.prototype.hasOwnProperty.call(item, key)) {
493
+ const value = item[key];
494
+ if (value && typeof value === 'object') {
495
+ processed[key] = deepUnwrap(value);
496
+ } else {
497
+ processed[key] = value;
498
+ }
499
+ }
500
+ }
501
+ return processed;
502
+ });
503
+ }
504
+
505
+ // Экспорт для Node.js совместимости
506
+ if (typeof module !== 'undefined' && module.exports) {
507
+ module.exports = {
508
+ jsonToCsv,
509
+ preprocessData,
510
+ deepUnwrap
511
+ };
512
+ }
513
+
514
+ // Браузерная версия CSV to JSON конвертера
515
+ // Адаптирована для работы в браузере без Node.js API
516
+
517
+
518
+ /**
519
+ * Валидация CSV ввода и опций
520
+ * @private
521
+ */
522
+ function validateCsvInput(csv, options) {
523
+ // Validate CSV input
524
+ if (typeof csv !== 'string') {
525
+ throw new ValidationError('Input must be a CSV string');
526
+ }
527
+
528
+ // Validate options
529
+ if (options && typeof options !== 'object') {
530
+ throw new ConfigurationError('Options must be an object');
531
+ }
532
+
533
+ // Validate delimiter
534
+ if (options !== null && options !== void 0 && options.delimiter && typeof options.delimiter !== 'string') {
535
+ throw new ConfigurationError('Delimiter must be a string');
536
+ }
537
+ if (options !== null && options !== void 0 && options.delimiter && options.delimiter.length !== 1) {
538
+ throw new ConfigurationError('Delimiter must be a single character');
539
+ }
540
+
541
+ // Validate autoDetect
542
+ if ((options === null || options === void 0 ? void 0 : options.autoDetect) !== undefined && typeof options.autoDetect !== 'boolean') {
543
+ throw new ConfigurationError('autoDetect must be a boolean');
544
+ }
545
+
546
+ // Validate candidates
547
+ if (options !== null && options !== void 0 && options.candidates && !Array.isArray(options.candidates)) {
548
+ throw new ConfigurationError('candidates must be an array');
549
+ }
550
+
551
+ // Validate maxRows
552
+ if ((options === null || options === void 0 ? void 0 : options.maxRows) !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
553
+ throw new ConfigurationError('maxRows must be a positive number');
554
+ }
555
+ return true;
556
+ }
557
+
558
+ /**
559
+ * Парсинг одной строки CSV с правильным экранированием
560
+ * @private
561
+ */
562
+ function parseCsvLine(line, lineNumber, delimiter) {
563
+ const fields = [];
564
+ let currentField = '';
565
+ let insideQuotes = false;
566
+ let escapeNext = false;
567
+ for (let i = 0; i < line.length; i++) {
568
+ const char = line[i];
569
+ if (escapeNext) {
570
+ currentField += char;
571
+ escapeNext = false;
572
+ continue;
573
+ }
574
+ if (char === '\\') {
575
+ if (i + 1 === line.length) {
576
+ // Обратный слеш в конце строки
577
+ currentField += char;
578
+ } else if (line[i + 1] === '\\') {
579
+ // Двойной обратный слеш
580
+ currentField += char;
581
+ i++; // Пропустить следующий слеш
582
+ } else {
583
+ // Экранирование следующего символа
584
+ escapeNext = true;
585
+ }
586
+ continue;
587
+ }
588
+ if (char === '"') {
589
+ if (insideQuotes) {
590
+ if (i + 1 < line.length && line[i + 1] === '"') {
591
+ // Экранированная кавычка внутри кавычек
592
+ currentField += '"';
593
+ i++; // Пропустить следующую кавычку
594
+
595
+ // Проверка конца поля
596
+ let isEndOfField = false;
597
+ let j = i + 1;
598
+ while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
599
+ j++;
600
+ }
601
+ if (j === line.length || line[j] === delimiter) {
602
+ isEndOfField = true;
603
+ }
604
+ if (isEndOfField) {
605
+ insideQuotes = false;
606
+ }
607
+ } else {
608
+ // Проверка конца поля
609
+ let isEndOfField = false;
610
+ let j = i + 1;
611
+ while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
612
+ j++;
613
+ }
614
+ if (j === line.length || line[j] === delimiter) {
615
+ isEndOfField = true;
616
+ }
617
+ if (isEndOfField) {
618
+ insideQuotes = false;
619
+ } else {
620
+ currentField += '"';
621
+ }
622
+ }
623
+ } else {
624
+ // Начало поля в кавычках
625
+ insideQuotes = true;
626
+ }
627
+ continue;
628
+ }
629
+ if (!insideQuotes && char === delimiter) {
630
+ // Конец поля
631
+ fields.push(currentField);
632
+ currentField = '';
633
+ continue;
634
+ }
635
+ currentField += char;
636
+ }
637
+
638
+ // Обработка незавершенного экранирования
639
+ if (escapeNext) {
640
+ currentField += '\\';
641
+ }
642
+
643
+ // Добавление последнего поля
644
+ fields.push(currentField);
645
+
646
+ // Проверка незакрытых кавычек
647
+ if (insideQuotes) {
648
+ throw new ParsingError('Unclosed quotes in CSV', lineNumber);
649
+ }
650
+
651
+ // Валидация количества полей
652
+ if (fields.length === 0) {
653
+ throw new ParsingError('No fields found', lineNumber);
654
+ }
655
+ return fields;
656
+ }
657
+
658
+ /**
659
+ * Парсинг значения на основе опций
660
+ * @private
661
+ */
662
+ function parseCsvValue(value, options) {
663
+ const {
664
+ trim = true,
665
+ parseNumbers = false,
666
+ parseBooleans = false
667
+ } = options;
668
+ let result = value;
669
+ if (trim) {
670
+ result = result.trim();
671
+ }
672
+
673
+ // Удаление защиты формул Excel
674
+ if (result.startsWith("'")) {
675
+ result = result.substring(1);
676
+ }
677
+
678
+ // Парсинг чисел
679
+ if (parseNumbers && /^-?\d+(\.\d+)?$/.test(result)) {
680
+ const num = parseFloat(result);
681
+ if (!isNaN(num)) {
682
+ return num;
683
+ }
684
+ }
685
+
686
+ // Парсинг булевых значений
687
+ if (parseBooleans) {
688
+ const lowerValue = result.toLowerCase();
689
+ if (lowerValue === 'true') {
690
+ return true;
691
+ }
692
+ if (lowerValue === 'false') {
693
+ return false;
694
+ }
695
+ }
696
+
697
+ // Пустые строки как null
698
+ if (result === '') {
699
+ return null;
700
+ }
701
+ return result;
702
+ }
703
+
704
+ /**
705
+ * Автоматическое определение разделителя CSV
706
+ *
707
+ * @param {string} csv - CSV строка
708
+ * @param {Array} [candidates=[';', ',', '\t', '|']] - Кандидаты на разделитель
709
+ * @returns {string} Определенный разделитель
710
+ */
711
+ function autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|']) {
712
+ if (!csv || typeof csv !== 'string') {
713
+ return ';'; // значение по умолчанию
714
+ }
715
+ const lines = csv.split('\n').filter(line => line.trim().length > 0);
716
+ if (lines.length === 0) {
717
+ return ';'; // значение по умолчанию
718
+ }
719
+
720
+ // Использование первой непустой строки для определения
721
+ const firstLine = lines[0];
722
+ const counts = {};
723
+ candidates.forEach(delim => {
724
+ const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
725
+ const regex = new RegExp(escapedDelim, 'g');
726
+ const matches = firstLine.match(regex);
727
+ counts[delim] = matches ? matches.length : 0;
728
+ });
729
+
730
+ // Поиск разделителя с максимальным количеством
731
+ let maxCount = -1;
732
+ let detectedDelimiter = ';'; // значение по умолчанию
733
+
734
+ for (const [delim, count] of Object.entries(counts)) {
735
+ if (count > maxCount) {
736
+ maxCount = count;
737
+ detectedDelimiter = delim;
738
+ }
739
+ }
740
+
741
+ // Если разделитель не найден или ничья
742
+ if (maxCount === 0) {
743
+ return ';'; // значение по умолчанию
744
+ }
745
+ return detectedDelimiter;
746
+ }
747
+
748
+ /**
749
+ * Конвертирует CSV строку в JSON массив
750
+ *
751
+ * @param {string} csv - CSV строка для конвертации
752
+ * @param {Object} [options] - Опции конфигурации
753
+ * @param {string} [options.delimiter] - CSV разделитель (по умолчанию: автоопределение)
754
+ * @param {boolean} [options.autoDetect=true] - Автоопределение разделителя
755
+ * @param {Array} [options.candidates=[';', ',', '\t', '|']] - Кандидаты для автоопределения
756
+ * @param {boolean} [options.hasHeaders=true] - Есть ли заголовки в CSV
757
+ * @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
758
+ * @param {boolean} [options.trim=true] - Обрезать пробелы
759
+ * @param {boolean} [options.parseNumbers=false] - Парсить числовые значения
760
+ * @param {boolean} [options.parseBooleans=false] - Парсить булевы значения
761
+ * @param {number} [options.maxRows] - Максимальное количество строк
762
+ * @returns {Array<Object>} JSON массив
763
+ */
764
+ function csvToJson(csv, options = {}) {
765
+ return safeExecute(() => {
766
+ // Валидация ввода
767
+ validateCsvInput(csv, options);
768
+ const opts = options && typeof options === 'object' ? options : {};
769
+ const {
770
+ delimiter,
771
+ autoDetect = true,
772
+ candidates = [';', ',', '\t', '|'],
773
+ hasHeaders = true,
774
+ renameMap = {},
775
+ trim = true,
776
+ parseNumbers = false,
777
+ parseBooleans = false,
778
+ maxRows
779
+ } = opts;
780
+
781
+ // Определение разделителя
782
+ let finalDelimiter = delimiter;
783
+ if (!finalDelimiter && autoDetect) {
784
+ finalDelimiter = autoDetectDelimiter(csv, candidates);
785
+ }
786
+ finalDelimiter = finalDelimiter || ';'; // fallback
787
+
788
+ // Обработка пустого CSV
789
+ if (csv.trim() === '') {
790
+ return [];
791
+ }
792
+
793
+ // Парсинг CSV с обработкой кавычек и переносов строк
794
+ const lines = [];
795
+ let currentLine = '';
796
+ let insideQuotes = false;
797
+ for (let i = 0; i < csv.length; i++) {
798
+ const char = csv[i];
799
+ if (char === '"') {
800
+ if (insideQuotes && i + 1 < csv.length && csv[i + 1] === '"') {
801
+ // Экранированная кавычка внутри кавычек
802
+ currentLine += '"';
803
+ i++; // Пропустить следующую кавычку
804
+ } else {
805
+ // Переключение режима кавычек
806
+ insideQuotes = !insideQuotes;
807
+ }
808
+ currentLine += char;
809
+ continue;
810
+ }
811
+ if (char === '\n' && !insideQuotes) {
812
+ // Конец строки (вне кавычек)
813
+ lines.push(currentLine);
814
+ currentLine = '';
815
+ continue;
816
+ }
817
+ if (char === '\r') {
818
+ // Игнорировать carriage return
819
+ continue;
820
+ }
821
+ currentLine += char;
822
+ }
823
+
824
+ // Добавление последней строки
825
+ if (currentLine !== '' || insideQuotes) {
826
+ lines.push(currentLine);
827
+ }
828
+ if (lines.length === 0) {
829
+ return [];
830
+ }
831
+
832
+ // Предупреждение для больших наборов данных
833
+ if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'production') {
834
+ console.warn('⚠️ Warning: Processing >1M records in memory may be slow.\n' + '💡 Consider using Web Workers for better performance with large files.\n' + '📊 Current size: ' + lines.length.toLocaleString() + ' rows');
835
+ }
836
+
837
+ // Применение ограничения по строкам
838
+ if (maxRows && lines.length > maxRows) {
839
+ throw new LimitError(`CSV size exceeds maximum limit of ${maxRows} rows`, maxRows, lines.length);
840
+ }
841
+ let headers = [];
842
+ let startIndex = 0;
843
+
844
+ // Парсинг заголовков если есть
845
+ if (hasHeaders && lines.length > 0) {
846
+ try {
847
+ headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
848
+ const trimmed = trim ? header.trim() : header;
849
+ return renameMap[trimmed] || trimmed;
850
+ });
851
+ startIndex = 1;
852
+ } catch (error) {
853
+ if (error instanceof ParsingError) {
854
+ throw new ParsingError(`Failed to parse headers: ${error.message}`, 1);
855
+ }
856
+ throw error;
857
+ }
858
+ } else {
859
+ // Генерация числовых заголовков из первой строки
860
+ try {
861
+ const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
862
+ headers = firstLineFields.map((_, index) => `column${index + 1}`);
863
+ } catch (error) {
864
+ if (error instanceof ParsingError) {
865
+ throw new ParsingError(`Failed to parse first line: ${error.message}`, 1);
866
+ }
867
+ throw error;
868
+ }
869
+ }
870
+
871
+ // Парсинг строк данных
872
+ const result = [];
873
+ for (let i = startIndex; i < lines.length; i++) {
874
+ const line = lines[i];
875
+
876
+ // Пропуск пустых строк
877
+ if (line.trim() === '') {
878
+ continue;
879
+ }
880
+ try {
881
+ const fields = parseCsvLine(line, i + 1, finalDelimiter);
882
+
883
+ // Обработка несоответствия количества полей
884
+ const row = {};
885
+ const fieldCount = Math.min(fields.length, headers.length);
886
+ for (let j = 0; j < fieldCount; j++) {
887
+ row[headers[j]] = parseCsvValue(fields[j], {
888
+ trim,
889
+ parseNumbers,
890
+ parseBooleans
891
+ });
892
+ }
893
+
894
+ // Предупреждение о лишних полях
895
+ if (fields.length > headers.length && process.env.NODE_ENV === 'development') {
896
+ console.warn(`[jtcsv] Line ${i + 1}: ${fields.length - headers.length} extra fields ignored`);
897
+ }
898
+ result.push(row);
899
+ } catch (error) {
900
+ if (error instanceof ParsingError) {
901
+ throw new ParsingError(`Line ${i + 1}: ${error.message}`, i + 1);
902
+ }
903
+ throw error;
904
+ }
905
+ }
906
+ return result;
907
+ }, 'PARSE_FAILED', {
908
+ function: 'csvToJson'
909
+ });
910
+ }
911
+
912
+ // Экспорт для Node.js совместимости
913
+ if (typeof module !== 'undefined' && module.exports) {
914
+ module.exports = {
915
+ csvToJson,
916
+ autoDetectDelimiter
917
+ };
918
+ }
919
+
920
+ // Браузерные специфичные функции для jtcsv
921
+ // Функции, которые работают только в браузере
922
+
923
+
924
+ /**
925
+ * Скачивает JSON данные как CSV файл
926
+ *
927
+ * @param {Array<Object>} data - Массив объектов для конвертации
928
+ * @param {string} [filename='data.csv'] - Имя файла для скачивания
929
+ * @param {Object} [options] - Опции для jsonToCsv
930
+ * @returns {void}
931
+ *
932
+ * @example
933
+ * const data = [
934
+ * { id: 1, name: 'John' },
935
+ * { id: 2, name: 'Jane' }
936
+ * ];
937
+ * downloadAsCsv(data, 'users.csv', { delimiter: ',' });
938
+ */
939
+ function downloadAsCsv(data, filename = 'data.csv', options = {}) {
940
+ // Проверка что мы в браузере
941
+ if (typeof window === 'undefined') {
942
+ throw new ValidationError('downloadAsCsv() работает только в браузере. Используйте saveAsCsv() в Node.js');
943
+ }
944
+
945
+ // Валидация имени файла
946
+ if (typeof filename !== 'string' || filename.trim() === '') {
947
+ throw new ValidationError('Filename must be a non-empty string');
948
+ }
949
+
950
+ // Добавление расширения .csv если его нет
951
+ if (!filename.toLowerCase().endsWith('.csv')) {
952
+ filename += '.csv';
953
+ }
954
+
955
+ // Конвертация в CSV
956
+ const csv = jsonToCsv(data, options);
957
+
958
+ // Создание Blob
959
+ const blob = new Blob([csv], {
960
+ type: 'text/csv;charset=utf-8;'
961
+ });
962
+
963
+ // Создание ссылки для скачивания
964
+ const link = document.createElement('a');
965
+
966
+ // Создание URL для Blob
967
+ const url = URL.createObjectURL(blob);
968
+
969
+ // Настройка ссылки
970
+ link.setAttribute('href', url);
971
+ link.setAttribute('download', filename);
972
+ link.style.visibility = 'hidden';
973
+
974
+ // Добавление в DOM и клик
975
+ document.body.appendChild(link);
976
+ link.click();
977
+
978
+ // Очистка
979
+ setTimeout(() => {
980
+ document.body.removeChild(link);
981
+ URL.revokeObjectURL(url);
982
+ }, 100);
983
+ }
984
+
985
+ /**
986
+ * Парсит CSV файл из input[type="file"] в JSON
987
+ *
988
+ * @param {File} file - File объект из input
989
+ * @param {Object} [options] - Опции для csvToJson
990
+ * @returns {Promise<Array<Object>>} Promise с JSON данными
991
+ *
992
+ * @example
993
+ * // HTML: <input type="file" id="csvFile" accept=".csv">
994
+ * const fileInput = document.getElementById('csvFile');
995
+ * const json = await parseCsvFile(fileInput.files[0], { delimiter: ',' });
996
+ */
997
+ async function parseCsvFile(file, options = {}) {
998
+ // Проверка что мы в браузере
999
+ if (typeof window === 'undefined') {
1000
+ throw new ValidationError('parseCsvFile() работает только в браузере. Используйте readCsvAsJson() в Node.js');
1001
+ }
1002
+
1003
+ // Валидация файла
1004
+ if (!(file instanceof File)) {
1005
+ throw new ValidationError('Input must be a File object');
1006
+ }
1007
+
1008
+ // Проверка расширения файла
1009
+ if (!file.name.toLowerCase().endsWith('.csv')) {
1010
+ throw new ValidationError('File must have .csv extension');
1011
+ }
1012
+
1013
+ // Проверка размера файла (предупреждение для больших файлов)
1014
+ const MAX_SIZE_WARNING = 50 * 1024 * 1024; // 50MB
1015
+ if (file.size > MAX_SIZE_WARNING && process.env.NODE_ENV !== 'production') {
1016
+ console.warn(`⚠️ Warning: Processing large file (${(file.size / 1024 / 1024).toFixed(2)}MB).\n` + '💡 Consider using Web Workers for better performance.\n' + '🔧 Tip: Use parseCSVWithWorker() for files > 10MB.');
1017
+ }
1018
+ return new Promise((resolve, reject) => {
1019
+ const reader = new FileReader();
1020
+ reader.onload = function (event) {
1021
+ try {
1022
+ const csvText = event.target.result;
1023
+ const json = csvToJson(csvText, options);
1024
+ resolve(json);
1025
+ } catch (error) {
1026
+ reject(error);
1027
+ }
1028
+ };
1029
+ reader.onerror = function () {
1030
+ reject(new ValidationError('Ошибка чтения файла'));
1031
+ };
1032
+ reader.onabort = function () {
1033
+ reject(new ValidationError('Чтение файла прервано'));
1034
+ };
1035
+
1036
+ // Чтение как текст
1037
+ reader.readAsText(file, 'UTF-8');
1038
+ });
1039
+ }
1040
+
1041
+ /**
1042
+ * Создает CSV файл из JSON данных (альтернатива downloadAsCsv)
1043
+ * Возвращает Blob вместо автоматического скачивания
1044
+ *
1045
+ * @param {Array<Object>} data - Массив объектов
1046
+ * @param {Object} [options] - Опции для jsonToCsv
1047
+ * @returns {Blob} CSV Blob
1048
+ */
1049
+ function createCsvBlob(data, options = {}) {
1050
+ const csv = jsonToCsv(data, options);
1051
+ return new Blob([csv], {
1052
+ type: 'text/csv;charset=utf-8;'
1053
+ });
1054
+ }
1055
+
1056
+ /**
1057
+ * Парсит CSV строку из Blob
1058
+ *
1059
+ * @param {Blob} blob - CSV Blob
1060
+ * @param {Object} [options] - Опции для csvToJson
1061
+ * @returns {Promise<Array<Object>>} Promise с JSON данными
1062
+ */
1063
+ async function parseCsvBlob(blob, options = {}) {
1064
+ if (!(blob instanceof Blob)) {
1065
+ throw new ValidationError('Input must be a Blob object');
1066
+ }
1067
+ return new Promise((resolve, reject) => {
1068
+ const reader = new FileReader();
1069
+ reader.onload = function (event) {
1070
+ try {
1071
+ const csvText = event.target.result;
1072
+ const json = csvToJson(csvText, options);
1073
+ resolve(json);
1074
+ } catch (error) {
1075
+ reject(error);
1076
+ }
1077
+ };
1078
+ reader.onerror = function () {
1079
+ reject(new ValidationError('Ошибка чтения Blob'));
1080
+ };
1081
+ reader.readAsText(blob, 'UTF-8');
1082
+ });
1083
+ }
1084
+
1085
+ // Экспорт для Node.js совместимости
1086
+ if (typeof module !== 'undefined' && module.exports) {
1087
+ module.exports = {
1088
+ downloadAsCsv,
1089
+ parseCsvFile,
1090
+ createCsvBlob,
1091
+ parseCsvBlob
1092
+ };
1093
+ }
1094
+
1095
+ // Worker Pool для параллельной обработки CSV
1096
+ // Использует Comlink для простой коммуникации с Web Workers
1097
+
1098
+
1099
+ // Проверка поддержки Web Workers
1100
+ const WORKERS_SUPPORTED = typeof Worker !== 'undefined';
1101
+
1102
+ /**
1103
+ * Опции для Worker Pool
1104
+ * @typedef {Object} WorkerPoolOptions
1105
+ * @property {number} [workerCount=4] - Количество workers в pool
1106
+ * @property {number} [maxQueueSize=100] - Максимальный размер очереди задач
1107
+ * @property {boolean} [autoScale=true] - Автоматическое масштабирование pool
1108
+ * @property {number} [idleTimeout=60000] - Таймаут простоя worker (мс)
1109
+ */
1110
+
1111
+ /**
1112
+ * Статистика Worker Pool
1113
+ * @typedef {Object} WorkerPoolStats
1114
+ * @property {number} totalWorkers - Всего workers
1115
+ * @property {number} activeWorkers - Активные workers
1116
+ * @property {number} idleWorkers - Простаивающие workers
1117
+ * @property {number} queueSize - Размер очереди
1118
+ * @property {number} tasksCompleted - Завершенные задачи
1119
+ * @property {number} tasksFailed - Неудачные задачи
1120
+ */
1121
+
1122
+ /**
1123
+ * Прогресс обработки задачи
1124
+ * @typedef {Object} TaskProgress
1125
+ * @property {number} processed - Обработано элементов
1126
+ * @property {number} total - Всего элементов
1127
+ * @property {number} percentage - Процент выполнения
1128
+ * @property {number} speed - Скорость обработки (элементов/сек)
1129
+ */
1130
+
1131
+ /**
1132
+ * Worker Pool для параллельной обработки CSV
1133
+ */
1134
+ class WorkerPool {
1135
+ /**
1136
+ * Создает новый Worker Pool
1137
+ * @param {string} workerScript - URL скрипта worker
1138
+ * @param {WorkerPoolOptions} [options] - Опции pool
1139
+ */
1140
+ constructor(workerScript, options = {}) {
1141
+ if (!WORKERS_SUPPORTED) {
1142
+ throw new ValidationError('Web Workers не поддерживаются в этом браузере');
1143
+ }
1144
+ this.workerScript = workerScript;
1145
+ this.options = {
1146
+ workerCount: 4,
1147
+ maxQueueSize: 100,
1148
+ autoScale: true,
1149
+ idleTimeout: 60000,
1150
+ ...options
1151
+ };
1152
+ this.workers = [];
1153
+ this.taskQueue = [];
1154
+ this.activeTasks = new Map();
1155
+ this.stats = {
1156
+ totalWorkers: 0,
1157
+ activeWorkers: 0,
1158
+ idleWorkers: 0,
1159
+ queueSize: 0,
1160
+ tasksCompleted: 0,
1161
+ tasksFailed: 0
1162
+ };
1163
+ this.initializeWorkers();
1164
+ }
1165
+
1166
+ /**
1167
+ * Инициализация workers
1168
+ * @private
1169
+ */
1170
+ initializeWorkers() {
1171
+ const {
1172
+ workerCount
1173
+ } = this.options;
1174
+ for (let i = 0; i < workerCount; i++) {
1175
+ this.createWorker();
1176
+ }
1177
+ this.updateStats();
1178
+ }
1179
+
1180
+ /**
1181
+ * Создает нового worker
1182
+ * @private
1183
+ */
1184
+ createWorker() {
1185
+ try {
1186
+ const worker = new Worker(this.workerScript, {
1187
+ type: 'module'
1188
+ });
1189
+ worker.id = `worker-${this.workers.length}`;
1190
+ worker.status = 'idle';
1191
+ worker.lastUsed = Date.now();
1192
+ worker.taskId = null;
1193
+
1194
+ // Обработчики событий
1195
+ worker.onmessage = event => this.handleWorkerMessage(worker, event);
1196
+ worker.onerror = error => this.handleWorkerError(worker, error);
1197
+ worker.onmessageerror = error => this.handleWorkerMessageError(worker, error);
1198
+ this.workers.push(worker);
1199
+ this.stats.totalWorkers++;
1200
+ this.stats.idleWorkers++;
1201
+ return worker;
1202
+ } catch (error) {
1203
+ throw new ConfigurationError(`Не удалось создать worker: ${error.message}`);
1204
+ }
1205
+ }
1206
+
1207
+ /**
1208
+ * Обработка сообщений от worker
1209
+ * @private
1210
+ */
1211
+ handleWorkerMessage(worker, event) {
1212
+ const {
1213
+ data
1214
+ } = event;
1215
+ if (data.type === 'PROGRESS') {
1216
+ this.handleProgress(worker, data);
1217
+ } else if (data.type === 'RESULT') {
1218
+ this.handleResult(worker, data);
1219
+ } else if (data.type === 'ERROR') {
1220
+ this.handleWorkerTaskError(worker, data);
1221
+ }
1222
+ }
1223
+
1224
+ /**
1225
+ * Обработка прогресса задачи
1226
+ * @private
1227
+ */
1228
+ handleProgress(worker, progressData) {
1229
+ const taskId = worker.taskId;
1230
+ if (taskId && this.activeTasks.has(taskId)) {
1231
+ const task = this.activeTasks.get(taskId);
1232
+ if (task.onProgress) {
1233
+ task.onProgress({
1234
+ processed: progressData.processed,
1235
+ total: progressData.total,
1236
+ percentage: progressData.processed / progressData.total * 100,
1237
+ speed: progressData.speed || 0
1238
+ });
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+ /**
1244
+ * Обработка результата задачи
1245
+ * @private
1246
+ */
1247
+ handleResult(worker, resultData) {
1248
+ const taskId = worker.taskId;
1249
+ if (taskId && this.activeTasks.has(taskId)) {
1250
+ const task = this.activeTasks.get(taskId);
1251
+
1252
+ // Освобождение worker
1253
+ worker.status = 'idle';
1254
+ worker.lastUsed = Date.now();
1255
+ worker.taskId = null;
1256
+ this.stats.activeWorkers--;
1257
+ this.stats.idleWorkers++;
1258
+
1259
+ // Завершение задачи
1260
+ task.resolve(resultData.data);
1261
+ this.activeTasks.delete(taskId);
1262
+ this.stats.tasksCompleted++;
1263
+
1264
+ // Обработка следующей задачи в очереди
1265
+ this.processQueue();
1266
+ this.updateStats();
1267
+ }
1268
+ }
1269
+
1270
+ /**
1271
+ * Обработка ошибки задачи
1272
+ * @private
1273
+ */
1274
+ handleWorkerTaskError(worker, errorData) {
1275
+ const taskId = worker.taskId;
1276
+ if (taskId && this.activeTasks.has(taskId)) {
1277
+ const task = this.activeTasks.get(taskId);
1278
+
1279
+ // Освобождение worker
1280
+ worker.status = 'idle';
1281
+ worker.lastUsed = Date.now();
1282
+ worker.taskId = null;
1283
+ this.stats.activeWorkers--;
1284
+ this.stats.idleWorkers++;
1285
+
1286
+ // Завершение с ошибкой
1287
+ task.reject(new Error(errorData.message || 'Ошибка в worker'));
1288
+ this.activeTasks.delete(taskId);
1289
+ this.stats.tasksFailed++;
1290
+
1291
+ // Обработка следующей задачи
1292
+ this.processQueue();
1293
+ this.updateStats();
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Обработка ошибок worker
1299
+ * @private
1300
+ */
1301
+ handleWorkerError(worker, error) {
1302
+ console.error(`Worker ${worker.id} error:`, error);
1303
+
1304
+ // Перезапуск worker
1305
+ this.restartWorker(worker);
1306
+ }
1307
+
1308
+ /**
1309
+ * Обработка ошибок сообщений
1310
+ * @private
1311
+ */
1312
+ handleWorkerMessageError(worker, error) {
1313
+ console.error(`Worker ${worker.id} message error:`, error);
1314
+ }
1315
+
1316
+ /**
1317
+ * Перезапуск worker
1318
+ * @private
1319
+ */
1320
+ restartWorker(worker) {
1321
+ const index = this.workers.indexOf(worker);
1322
+ if (index !== -1) {
1323
+ // Завершение старого worker
1324
+ worker.terminate();
1325
+
1326
+ // Удаление из статистики
1327
+ if (worker.status === 'active') {
1328
+ this.stats.activeWorkers--;
1329
+ } else {
1330
+ this.stats.idleWorkers--;
1331
+ }
1332
+ this.stats.totalWorkers--;
1333
+
1334
+ // Создание нового worker
1335
+ const newWorker = this.createWorker();
1336
+ this.workers[index] = newWorker;
1337
+
1338
+ // Перезапуск задачи если была активна
1339
+ if (worker.taskId && this.activeTasks.has(worker.taskId)) {
1340
+ const task = this.activeTasks.get(worker.taskId);
1341
+ this.executeTask(newWorker, task);
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ /**
1347
+ * Выполнение задачи на worker
1348
+ * @private
1349
+ */
1350
+ executeTask(worker, task) {
1351
+ worker.status = 'active';
1352
+ worker.lastUsed = Date.now();
1353
+ worker.taskId = task.id;
1354
+ this.stats.idleWorkers--;
1355
+ this.stats.activeWorkers++;
1356
+
1357
+ // Отправка задачи в worker
1358
+ worker.postMessage({
1359
+ type: 'EXECUTE',
1360
+ taskId: task.id,
1361
+ method: task.method,
1362
+ args: task.args,
1363
+ options: task.options
1364
+ });
1365
+ }
1366
+
1367
+ /**
1368
+ * Обработка очереди задач
1369
+ * @private
1370
+ */
1371
+ processQueue() {
1372
+ if (this.taskQueue.length === 0) {
1373
+ return;
1374
+ }
1375
+
1376
+ // Поиск свободного worker
1377
+ const idleWorker = this.workers.find(w => w.status === 'idle');
1378
+ if (!idleWorker) {
1379
+ // Автомасштабирование если включено
1380
+ if (this.options.autoScale && this.workers.length < this.options.maxQueueSize) {
1381
+ this.createWorker();
1382
+ this.processQueue();
1383
+ }
1384
+ return;
1385
+ }
1386
+
1387
+ // Получение задачи из очереди
1388
+ const task = this.taskQueue.shift();
1389
+ this.stats.queueSize--;
1390
+
1391
+ // Выполнение задачи
1392
+ this.executeTask(idleWorker, task);
1393
+ this.updateStats();
1394
+ }
1395
+
1396
+ /**
1397
+ * Обновление статистики
1398
+ * @private
1399
+ */
1400
+ updateStats() {
1401
+ this.stats.queueSize = this.taskQueue.length;
1402
+ }
1403
+
1404
+ /**
1405
+ * Выполнение задачи через pool
1406
+ * @param {string} method - Метод для вызова в worker
1407
+ * @param {Array} args - Аргументы метода
1408
+ * @param {Object} [options] - Опции задачи
1409
+ * @param {Function} [onProgress] - Callback прогресса
1410
+ * @returns {Promise<any>} Результат выполнения
1411
+ */
1412
+ async exec(method, args = [], options = {}, onProgress = null) {
1413
+ return new Promise((resolve, reject) => {
1414
+ // Проверка размера очереди
1415
+ if (this.taskQueue.length >= this.options.maxQueueSize) {
1416
+ reject(new Error('Очередь задач переполнена'));
1417
+ return;
1418
+ }
1419
+
1420
+ // Создание задачи
1421
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1422
+ const task = {
1423
+ id: taskId,
1424
+ method,
1425
+ args,
1426
+ options,
1427
+ onProgress,
1428
+ resolve,
1429
+ reject,
1430
+ createdAt: Date.now()
1431
+ };
1432
+
1433
+ // Добавление в очередь
1434
+ this.taskQueue.push(task);
1435
+ this.stats.queueSize++;
1436
+
1437
+ // Запуск обработки очереди
1438
+ this.processQueue();
1439
+ this.updateStats();
1440
+ });
1441
+ }
1442
+
1443
+ /**
1444
+ * Получение статистики pool
1445
+ * @returns {WorkerPoolStats} Статистика
1446
+ */
1447
+ getStats() {
1448
+ return {
1449
+ ...this.stats
1450
+ };
1451
+ }
1452
+
1453
+ /**
1454
+ * Очистка простаивающих workers
1455
+ */
1456
+ cleanupIdleWorkers() {
1457
+ const now = Date.now();
1458
+ const {
1459
+ idleTimeout
1460
+ } = this.options;
1461
+ for (let i = this.workers.length - 1; i >= 0; i--) {
1462
+ const worker = this.workers[i];
1463
+ if (worker.status === 'idle' && now - worker.lastUsed > idleTimeout) {
1464
+ // Сохранение минимального количества workers
1465
+ if (this.workers.length > 1) {
1466
+ worker.terminate();
1467
+ this.workers.splice(i, 1);
1468
+ this.stats.totalWorkers--;
1469
+ this.stats.idleWorkers--;
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+
1475
+ /**
1476
+ * Завершение всех workers
1477
+ */
1478
+ terminate() {
1479
+ this.workers.forEach(worker => {
1480
+ worker.terminate();
1481
+ });
1482
+ this.workers = [];
1483
+ this.taskQueue = [];
1484
+ this.activeTasks.clear();
1485
+
1486
+ // Сброс статистики
1487
+ this.stats = {
1488
+ totalWorkers: 0,
1489
+ activeWorkers: 0,
1490
+ idleWorkers: 0,
1491
+ queueSize: 0,
1492
+ tasksCompleted: 0,
1493
+ tasksFailed: 0
1494
+ };
1495
+ }
1496
+ }
1497
+
1498
+ /**
1499
+ * Создает Worker Pool для обработки CSV
1500
+ * @param {WorkerPoolOptions} [options] - Опции pool
1501
+ * @returns {WorkerPool} Worker Pool
1502
+ */
1503
+ function createWorkerPool(options = {}) {
1504
+ // Используем встроенный worker скрипт
1505
+ const workerScript = new URL('./csv-parser.worker.js', (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('jtcsv.cjs.js', document.baseURI).href))).href;
1506
+ return new WorkerPool(workerScript, options);
1507
+ }
1508
+
1509
+ /**
1510
+ * Парсит CSV с использованием Web Workers
1511
+ * @param {string|File} csvInput - CSV строка или File объект
1512
+ * @param {Object} [options] - Опции парсинга
1513
+ * @param {Function} [onProgress] - Callback прогресса
1514
+ * @returns {Promise<Array<Object>>} JSON данные
1515
+ */
1516
+ async function parseCSVWithWorker(csvInput, options = {}, onProgress = null) {
1517
+ // Создание pool если нужно
1518
+ if (!parseCSVWithWorker.pool) {
1519
+ parseCSVWithWorker.pool = createWorkerPool();
1520
+ }
1521
+ const pool = parseCSVWithWorker.pool;
1522
+
1523
+ // Подготовка CSV строки
1524
+ let csvString;
1525
+ if (csvInput instanceof File) {
1526
+ csvString = await readFileAsText(csvInput);
1527
+ } else if (typeof csvInput === 'string') {
1528
+ csvString = csvInput;
1529
+ } else {
1530
+ throw new ValidationError('Input must be a CSV string or File object');
1531
+ }
1532
+
1533
+ // Выполнение через pool
1534
+ return pool.exec('parseCSV', [csvString, options], {}, onProgress);
1535
+ }
1536
+
1537
+ /**
1538
+ * Чтение файла как текст
1539
+ * @private
1540
+ */
1541
+ async function readFileAsText(file) {
1542
+ return new Promise((resolve, reject) => {
1543
+ const reader = new FileReader();
1544
+ reader.onload = event => resolve(event.target.result);
1545
+ reader.onerror = error => reject(error);
1546
+ reader.readAsText(file, 'UTF-8');
1547
+ });
1548
+ }
1549
+
1550
+ // Экспорт для Node.js совместимости
1551
+ if (typeof module !== 'undefined' && module.exports) {
1552
+ module.exports = {
1553
+ WorkerPool,
1554
+ createWorkerPool,
1555
+ parseCSVWithWorker
1556
+ };
1557
+ }
1558
+
1559
+ // Браузерный entry point для jtcsv
1560
+ // Экспортирует все функции с поддержкой браузера
1561
+
1562
+
1563
+ // Основной экспорт
1564
+ const jtcsv = {
1565
+ // JSON to CSV функции
1566
+ jsonToCsv,
1567
+ preprocessData,
1568
+ downloadAsCsv,
1569
+ deepUnwrap,
1570
+ // CSV to JSON функции
1571
+ csvToJson,
1572
+ parseCsvFile,
1573
+ autoDetectDelimiter,
1574
+ // Web Workers функции
1575
+ createWorkerPool,
1576
+ parseCSVWithWorker,
1577
+ // Error classes
1578
+ ValidationError,
1579
+ SecurityError,
1580
+ FileSystemError,
1581
+ ParsingError,
1582
+ LimitError,
1583
+ ConfigurationError,
1584
+ // Удобные алиасы
1585
+ parse: csvToJson,
1586
+ unparse: jsonToCsv,
1587
+ // Версия
1588
+ version: '2.0.0-browser'
1589
+ };
1590
+
1591
+ // Экспорт для разных сред
1592
+ if (typeof module !== 'undefined' && module.exports) {
1593
+ // Node.js CommonJS
1594
+ module.exports = jtcsv;
1595
+ } else if (typeof define === 'function' && define.amd) {
1596
+ // AMD
1597
+ define([], () => jtcsv);
1598
+ } else if (typeof window !== 'undefined') {
1599
+ // Браузер (глобальная переменная)
1600
+ window.jtcsv = jtcsv;
1601
+ }
1602
+
1603
+ exports.ConfigurationError = ConfigurationError;
1604
+ exports.FileSystemError = FileSystemError;
1605
+ exports.LimitError = LimitError;
1606
+ exports.ParsingError = ParsingError;
1607
+ exports.SecurityError = SecurityError;
1608
+ exports.ValidationError = ValidationError;
1609
+ exports.autoDetectDelimiter = autoDetectDelimiter;
1610
+ exports.createWorkerPool = createWorkerPool;
1611
+ exports.csvToJson = csvToJson;
1612
+ exports.deepUnwrap = deepUnwrap;
1613
+ exports.default = jtcsv;
1614
+ exports.downloadAsCsv = downloadAsCsv;
1615
+ exports.jsonToCsv = jsonToCsv;
1616
+ exports.parseCSVWithWorker = parseCSVWithWorker;
1617
+ exports.parseCsvFile = parseCsvFile;
1618
+ exports.preprocessData = preprocessData;
1619
+ //# sourceMappingURL=jtcsv.cjs.js.map