jtcsv 2.2.7 → 3.0.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.
- package/README.md +31 -1
- package/bin/jtcsv.js +891 -821
- package/bin/jtcsv.ts +2534 -0
- package/csv-to-json.js +168 -145
- package/dist/jtcsv-core.cjs.js +1407 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1379 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1413 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +1912 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +1880 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +1918 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +759 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +773 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +61 -19
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +61 -19
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +61 -19
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +188 -2
- package/examples/advanced/conditional-transformations.js +446 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.js +89 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.js +306 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.js +504 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.js +116 -0
- package/examples/advanced/run-demo-server.ts +116 -0
- package/examples/advanced/web-worker-usage.html +874 -0
- package/examples/async-multithreaded-example.ts +335 -0
- package/examples/cli-advanced-usage.md +288 -0
- package/examples/cli-batch-processing.ts +38 -0
- package/examples/cli-tool.js +0 -3
- package/examples/cli-tool.ts +183 -0
- package/examples/error-handling.js +21 -7
- package/examples/error-handling.ts +356 -0
- package/examples/express-api.js +0 -3
- package/examples/express-api.ts +164 -0
- package/examples/large-dataset-example.js +0 -3
- package/examples/large-dataset-example.ts +204 -0
- package/examples/ndjson-processing.js +1 -1
- package/examples/ndjson-processing.ts +456 -0
- package/examples/plugin-excel-exporter.js +3 -4
- package/examples/plugin-excel-exporter.ts +406 -0
- package/examples/react-integration.tsx +637 -0
- package/examples/schema-validation.ts +640 -0
- package/examples/simple-usage.js +254 -254
- package/examples/simple-usage.ts +194 -0
- package/examples/streaming-example.js +4 -5
- package/examples/streaming-example.ts +419 -0
- package/examples/web-workers-advanced.ts +28 -0
- package/index.d.ts +1 -3
- package/index.js +15 -1
- package/json-save.js +9 -3
- package/json-to-csv.js +168 -21
- package/package.json +69 -10
- package/plugins/express-middleware/README.md +21 -2
- package/plugins/express-middleware/example.js +3 -4
- package/plugins/express-middleware/example.ts +135 -0
- package/plugins/express-middleware/index.d.ts +1 -1
- package/plugins/express-middleware/index.js +270 -118
- package/plugins/express-middleware/index.ts +557 -0
- package/plugins/fastify-plugin/index.js +2 -4
- package/plugins/fastify-plugin/index.ts +443 -0
- package/plugins/hono/index.ts +226 -0
- package/plugins/nestjs/index.ts +201 -0
- package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +0 -2
- package/plugins/nextjs-api/examples/api-convert.ts +67 -0
- package/plugins/nextjs-api/index.tsx +339 -0
- package/plugins/nextjs-api/route.js +2 -3
- package/plugins/nextjs-api/route.ts +370 -0
- package/plugins/nuxt/index.ts +94 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
- package/plugins/nuxt/runtime/plugin.ts +71 -0
- package/plugins/remix/index.js +1 -1
- package/plugins/remix/index.ts +260 -0
- package/plugins/sveltekit/index.js +1 -1
- package/plugins/sveltekit/index.ts +301 -0
- package/plugins/trpc/index.ts +267 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.js +92 -0
- package/src/browser/core.ts +152 -0
- package/src/browser/csv-to-json-browser.d.ts +3 -0
- package/src/browser/csv-to-json-browser.js +36 -14
- package/src/browser/csv-to-json-browser.ts +264 -0
- package/src/browser/errors-browser.ts +303 -0
- package/src/browser/extensions/plugins.js +92 -0
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.js +39 -0
- package/src/browser/extensions/workers.ts +39 -0
- package/src/browser/globals.d.ts +5 -0
- package/src/browser/index.ts +192 -0
- package/src/browser/json-to-csv-browser.d.ts +3 -0
- package/src/browser/json-to-csv-browser.js +13 -3
- package/src/browser/json-to-csv-browser.ts +262 -0
- package/src/browser/streams.js +12 -2
- package/src/browser/streams.ts +336 -0
- package/src/browser/workers/csv-parser.worker.ts +377 -0
- package/src/browser/workers/worker-pool.ts +548 -0
- package/src/core/delimiter-cache.js +22 -8
- package/src/core/delimiter-cache.ts +310 -0
- package/src/core/node-optimizations.ts +449 -0
- package/src/core/plugin-system.js +29 -11
- package/src/core/plugin-system.ts +400 -0
- package/src/core/transform-hooks.ts +558 -0
- package/src/engines/fast-path-engine-new.ts +347 -0
- package/src/engines/fast-path-engine.ts +854 -0
- package/src/errors.ts +72 -0
- package/src/formats/ndjson-parser.ts +469 -0
- package/src/formats/tsv-parser.ts +334 -0
- package/src/index-with-plugins.js +16 -9
- package/src/index-with-plugins.ts +395 -0
- package/src/types/index.ts +255 -0
- package/src/utils/bom-utils.js +259 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.js +124 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/schema-validator.js +19 -19
- package/src/utils/schema-validator.ts +819 -0
- package/src/utils/transform-loader.js +1 -1
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/zod-adapter.js +170 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/index.js +10 -10
- package/src/web-server/index.ts +683 -0
- package/src/workers/csv-multithreaded.ts +310 -0
- package/src/workers/csv-parser.worker.ts +227 -0
- package/src/workers/worker-pool.ts +409 -0
- package/stream-csv-to-json.js +26 -8
- package/stream-json-to-csv.js +1 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// Worker Pool для параллельной обработки CSV
|
|
2
|
+
// Использует Comlink для простой коммуникации с Web Workers
|
|
3
|
+
|
|
4
|
+
import { ValidationError, ConfigurationError } from '../errors-browser';
|
|
5
|
+
|
|
6
|
+
// Проверка поддержки Web Workers
|
|
7
|
+
const WORKERS_SUPPORTED = typeof Worker !== 'undefined';
|
|
8
|
+
|
|
9
|
+
function isTransferableBuffer(value) {
|
|
10
|
+
if (!(value instanceof ArrayBuffer)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectTransferables(args) {
|
|
20
|
+
const transferables = [];
|
|
21
|
+
|
|
22
|
+
const collectFromValue = (value) => {
|
|
23
|
+
if (!value) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (isTransferableBuffer(value)) {
|
|
27
|
+
transferables.push(value);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (ArrayBuffer.isView(value) && isTransferableBuffer(value.buffer)) {
|
|
31
|
+
transferables.push(value.buffer);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
value.forEach(collectFromValue);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
args.forEach(collectFromValue);
|
|
40
|
+
return transferables.length ? transferables : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type WorkerWithMeta = Worker & {
|
|
44
|
+
id: string;
|
|
45
|
+
status: 'idle' | 'busy' | 'error';
|
|
46
|
+
lastUsed: number;
|
|
47
|
+
taskId: string | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Опции для Worker Pool
|
|
52
|
+
* @typedef {Object} WorkerPoolOptions
|
|
53
|
+
* @property {number} [workerCount=4] - Количество workers в pool
|
|
54
|
+
* @property {number} [maxQueueSize=100] - Максимальный размер очереди задач
|
|
55
|
+
* @property {boolean} [autoScale=true] - Автоматическое масштабирование pool
|
|
56
|
+
* @property {number} [idleTimeout=60000] - Таймаут простоя worker (мс)
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Статистика Worker Pool
|
|
61
|
+
* @typedef {Object} WorkerPoolStats
|
|
62
|
+
* @property {number} totalWorkers - Всего workers
|
|
63
|
+
* @property {number} activeWorkers - Активные workers
|
|
64
|
+
* @property {number} idleWorkers - Простаивающие workers
|
|
65
|
+
* @property {number} queueSize - Размер очереди
|
|
66
|
+
* @property {number} tasksCompleted - Завершенные задачи
|
|
67
|
+
* @property {number} tasksFailed - Неудачные задачи
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Прогресс обработки задачи
|
|
72
|
+
* @typedef {Object} TaskProgress
|
|
73
|
+
* @property {number} processed - Обработано элементов
|
|
74
|
+
* @property {number} total - Всего элементов
|
|
75
|
+
* @property {number} percentage - Процент выполнения
|
|
76
|
+
* @property {number} speed - Скорость обработки (элементов/сек)
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Worker Pool для параллельной обработки CSV
|
|
81
|
+
*/
|
|
82
|
+
export class WorkerPool {
|
|
83
|
+
workerScript: string;
|
|
84
|
+
options: any;
|
|
85
|
+
workers: WorkerWithMeta[];
|
|
86
|
+
taskQueue: any[];
|
|
87
|
+
activeTasks: Map<any, any>;
|
|
88
|
+
stats: any;
|
|
89
|
+
/**
|
|
90
|
+
* Создает новый Worker Pool
|
|
91
|
+
* @param {string} workerScript - URL скрипта worker
|
|
92
|
+
* @param {WorkerPoolOptions} [options] - Опции pool
|
|
93
|
+
*/
|
|
94
|
+
constructor(workerScript, options = {}) {
|
|
95
|
+
if (!WORKERS_SUPPORTED) {
|
|
96
|
+
throw new ValidationError('Web Workers не поддерживаются в этом браузере');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.workerScript = workerScript;
|
|
100
|
+
this.options = {
|
|
101
|
+
workerCount: 4,
|
|
102
|
+
maxQueueSize: 100,
|
|
103
|
+
autoScale: true,
|
|
104
|
+
idleTimeout: 60000,
|
|
105
|
+
...options
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this.workers = [];
|
|
109
|
+
this.taskQueue = [];
|
|
110
|
+
this.activeTasks = new Map();
|
|
111
|
+
this.stats = {
|
|
112
|
+
totalWorkers: 0,
|
|
113
|
+
activeWorkers: 0,
|
|
114
|
+
idleWorkers: 0,
|
|
115
|
+
queueSize: 0,
|
|
116
|
+
tasksCompleted: 0,
|
|
117
|
+
tasksFailed: 0
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.initializeWorkers();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Инициализация workers
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
initializeWorkers() {
|
|
128
|
+
const { workerCount } = this.options;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < workerCount; i++) {
|
|
131
|
+
this.createWorker();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.updateStats();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Создает нового worker
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
createWorker() {
|
|
142
|
+
try {
|
|
143
|
+
const worker = new Worker(this.workerScript, { type: 'module' }) as WorkerWithMeta;
|
|
144
|
+
|
|
145
|
+
worker.id = `worker-${this.workers.length}`;
|
|
146
|
+
worker.status = 'idle';
|
|
147
|
+
worker.lastUsed = Date.now();
|
|
148
|
+
worker.taskId = null;
|
|
149
|
+
|
|
150
|
+
// Обработчики событий
|
|
151
|
+
worker.onmessage = (event) => this.handleWorkerMessage(worker, event);
|
|
152
|
+
worker.onerror = (error) => this.handleWorkerError(worker, error);
|
|
153
|
+
worker.onmessageerror = (error) => this.handleWorkerMessageError(worker, error);
|
|
154
|
+
|
|
155
|
+
this.workers.push(worker);
|
|
156
|
+
this.stats.totalWorkers++;
|
|
157
|
+
this.stats.idleWorkers++;
|
|
158
|
+
|
|
159
|
+
return worker;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new ConfigurationError(`Не удалось создать worker: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Обработка сообщений от worker
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
handleWorkerMessage(worker, event) {
|
|
170
|
+
const { data } = event;
|
|
171
|
+
|
|
172
|
+
if (data.type === 'PROGRESS') {
|
|
173
|
+
this.handleProgress(worker, data);
|
|
174
|
+
} else if (data.type === 'RESULT') {
|
|
175
|
+
this.handleResult(worker, data);
|
|
176
|
+
} else if (data.type === 'ERROR') {
|
|
177
|
+
this.handleWorkerTaskError(worker, data);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Обработка прогресса задачи
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
handleProgress(worker, progressData) {
|
|
186
|
+
const taskId = worker.taskId;
|
|
187
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
188
|
+
const task = this.activeTasks.get(taskId);
|
|
189
|
+
if (task.onProgress) {
|
|
190
|
+
task.onProgress({
|
|
191
|
+
processed: progressData.processed,
|
|
192
|
+
total: progressData.total,
|
|
193
|
+
percentage: (progressData.processed / progressData.total) * 100,
|
|
194
|
+
speed: progressData.speed || 0
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Обработка результата задачи
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
handleResult(worker, resultData) {
|
|
205
|
+
const taskId = worker.taskId;
|
|
206
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
207
|
+
const task = this.activeTasks.get(taskId);
|
|
208
|
+
|
|
209
|
+
// Освобождение worker
|
|
210
|
+
worker.status = 'idle';
|
|
211
|
+
worker.lastUsed = Date.now();
|
|
212
|
+
worker.taskId = null;
|
|
213
|
+
this.stats.activeWorkers--;
|
|
214
|
+
this.stats.idleWorkers++;
|
|
215
|
+
|
|
216
|
+
// Завершение задачи
|
|
217
|
+
task.resolve(resultData.data);
|
|
218
|
+
this.activeTasks.delete(taskId);
|
|
219
|
+
this.stats.tasksCompleted++;
|
|
220
|
+
|
|
221
|
+
// Обработка следующей задачи в очереди
|
|
222
|
+
this.processQueue();
|
|
223
|
+
this.updateStats();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Обработка ошибки задачи
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
handleWorkerTaskError(worker, errorData) {
|
|
232
|
+
const taskId = worker.taskId;
|
|
233
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
234
|
+
const task = this.activeTasks.get(taskId);
|
|
235
|
+
|
|
236
|
+
// Освобождение worker
|
|
237
|
+
worker.status = 'idle';
|
|
238
|
+
worker.lastUsed = Date.now();
|
|
239
|
+
worker.taskId = null;
|
|
240
|
+
this.stats.activeWorkers--;
|
|
241
|
+
this.stats.idleWorkers++;
|
|
242
|
+
|
|
243
|
+
// Завершение с ошибкой
|
|
244
|
+
const workerError = new Error(errorData.message || 'Ошибка в worker');
|
|
245
|
+
if ((errorData as any).code) {
|
|
246
|
+
(workerError as any).code = (errorData as any).code;
|
|
247
|
+
}
|
|
248
|
+
if ((errorData as any).details) {
|
|
249
|
+
(workerError as any).details = (errorData as any).details;
|
|
250
|
+
}
|
|
251
|
+
task.reject(workerError);
|
|
252
|
+
this.activeTasks.delete(taskId);
|
|
253
|
+
this.stats.tasksFailed++;
|
|
254
|
+
|
|
255
|
+
// Обработка следующей задачи
|
|
256
|
+
this.processQueue();
|
|
257
|
+
this.updateStats();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Обработка ошибок worker
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
handleWorkerError(worker, error) {
|
|
266
|
+
console.error(`Worker ${worker.id} error:`, error);
|
|
267
|
+
|
|
268
|
+
// Перезапуск worker
|
|
269
|
+
this.restartWorker(worker);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Обработка ошибок сообщений
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
handleWorkerMessageError(worker, error) {
|
|
277
|
+
console.error(`Worker ${worker.id} message error:`, error);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Перезапуск worker
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
restartWorker(worker) {
|
|
285
|
+
const index = this.workers.indexOf(worker);
|
|
286
|
+
if (index !== -1) {
|
|
287
|
+
// Завершение старого worker
|
|
288
|
+
worker.terminate();
|
|
289
|
+
|
|
290
|
+
// Удаление из статистики
|
|
291
|
+
if (worker.status === 'active') {
|
|
292
|
+
this.stats.activeWorkers--;
|
|
293
|
+
} else {
|
|
294
|
+
this.stats.idleWorkers--;
|
|
295
|
+
}
|
|
296
|
+
this.stats.totalWorkers--;
|
|
297
|
+
|
|
298
|
+
// Создание нового worker
|
|
299
|
+
const newWorker = this.createWorker();
|
|
300
|
+
this.workers[index] = newWorker;
|
|
301
|
+
|
|
302
|
+
// Перезапуск задачи если была активна
|
|
303
|
+
if (worker.taskId && this.activeTasks.has(worker.taskId)) {
|
|
304
|
+
const task = this.activeTasks.get(worker.taskId);
|
|
305
|
+
this.executeTask(newWorker, task);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Выполнение задачи на worker
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
executeTask(worker, task) {
|
|
315
|
+
worker.status = 'active';
|
|
316
|
+
worker.lastUsed = Date.now();
|
|
317
|
+
worker.taskId = task.id;
|
|
318
|
+
|
|
319
|
+
this.stats.idleWorkers--;
|
|
320
|
+
this.stats.activeWorkers++;
|
|
321
|
+
|
|
322
|
+
// Отправка задачи в worker
|
|
323
|
+
const payload = {
|
|
324
|
+
type: 'EXECUTE',
|
|
325
|
+
taskId: task.id,
|
|
326
|
+
method: task.method,
|
|
327
|
+
args: task.args,
|
|
328
|
+
options: task.options
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (task.transferList && task.transferList.length) {
|
|
332
|
+
worker.postMessage(payload, task.transferList);
|
|
333
|
+
} else {
|
|
334
|
+
worker.postMessage(payload);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Обработка очереди задач
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
processQueue() {
|
|
343
|
+
if (this.taskQueue.length === 0) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
while (this.taskQueue.length > 0) {
|
|
348
|
+
const idleWorker = this.workers.find(w => w.status === 'idle');
|
|
349
|
+
if (!idleWorker) {
|
|
350
|
+
if (this.options.autoScale && this.workers.length < this.options.maxQueueSize) {
|
|
351
|
+
this.createWorker();
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const task = this.taskQueue.shift();
|
|
358
|
+
this.stats.queueSize--;
|
|
359
|
+
this.executeTask(idleWorker, task);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.updateStats();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Обновление статистики
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
updateStats() {
|
|
370
|
+
this.stats.queueSize = this.taskQueue.length;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Выполнение задачи через pool
|
|
375
|
+
* @param {string} method - Метод для вызова в worker
|
|
376
|
+
* @param {Array} args - Аргументы метода
|
|
377
|
+
* @param {Object} [options] - Опции задачи
|
|
378
|
+
* @param {Function} [onProgress] - Callback прогресса
|
|
379
|
+
* @returns {Promise<unknown>} Результат выполнения
|
|
380
|
+
*/
|
|
381
|
+
async exec(method, args = [], options: any = {}, onProgress = null) {
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
// Проверка размера очереди
|
|
384
|
+
if (this.taskQueue.length >= this.options.maxQueueSize) {
|
|
385
|
+
reject(new Error('Очередь задач переполнена'));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Создание задачи
|
|
390
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
391
|
+
const { transfer, ...taskOptions } = options || {};
|
|
392
|
+
const transferList = transfer || collectTransferables(args);
|
|
393
|
+
const task = {
|
|
394
|
+
id: taskId,
|
|
395
|
+
method,
|
|
396
|
+
args,
|
|
397
|
+
options: taskOptions,
|
|
398
|
+
transferList,
|
|
399
|
+
onProgress,
|
|
400
|
+
resolve,
|
|
401
|
+
reject,
|
|
402
|
+
createdAt: Date.now()
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Добавление в очередь
|
|
406
|
+
this.taskQueue.push(task);
|
|
407
|
+
this.stats.queueSize++;
|
|
408
|
+
|
|
409
|
+
// Запуск обработки очереди
|
|
410
|
+
this.processQueue();
|
|
411
|
+
this.updateStats();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Получение статистики pool
|
|
417
|
+
* @returns {WorkerPoolStats} Статистика
|
|
418
|
+
*/
|
|
419
|
+
getStats() {
|
|
420
|
+
return { ...this.stats };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Очистка простаивающих workers
|
|
425
|
+
*/
|
|
426
|
+
cleanupIdleWorkers() {
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const { idleTimeout } = this.options;
|
|
429
|
+
|
|
430
|
+
for (let i = this.workers.length - 1; i >= 0; i--) {
|
|
431
|
+
const worker = this.workers[i];
|
|
432
|
+
if (worker.status === 'idle' && (now - worker.lastUsed) > idleTimeout) {
|
|
433
|
+
// Сохранение минимального количества workers
|
|
434
|
+
if (this.workers.length > 1) {
|
|
435
|
+
worker.terminate();
|
|
436
|
+
this.workers.splice(i, 1);
|
|
437
|
+
this.stats.totalWorkers--;
|
|
438
|
+
this.stats.idleWorkers--;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Завершение всех workers
|
|
446
|
+
*/
|
|
447
|
+
terminate() {
|
|
448
|
+
this.workers.forEach(worker => {
|
|
449
|
+
worker.terminate();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
this.workers = [];
|
|
453
|
+
this.taskQueue = [];
|
|
454
|
+
this.activeTasks.clear();
|
|
455
|
+
|
|
456
|
+
// Сброс статистики
|
|
457
|
+
this.stats = {
|
|
458
|
+
totalWorkers: 0,
|
|
459
|
+
activeWorkers: 0,
|
|
460
|
+
idleWorkers: 0,
|
|
461
|
+
queueSize: 0,
|
|
462
|
+
tasksCompleted: 0,
|
|
463
|
+
tasksFailed: 0
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Создает Worker Pool для обработки CSV
|
|
470
|
+
* @param {WorkerPoolOptions} [options] - Опции pool
|
|
471
|
+
* @returns {WorkerPool} Worker Pool
|
|
472
|
+
*/
|
|
473
|
+
export function createWorkerPool(options: any = {}): any {
|
|
474
|
+
// Используем встроенный worker скрипт
|
|
475
|
+
const baseUrl =
|
|
476
|
+
typeof document !== 'undefined'
|
|
477
|
+
? document.baseURI
|
|
478
|
+
: (typeof self !== 'undefined' && (self as any).location
|
|
479
|
+
? (self as any).location.href
|
|
480
|
+
: '');
|
|
481
|
+
const workerScript = new URL('./csv-parser.worker.js', baseUrl).href;
|
|
482
|
+
return new WorkerPool(workerScript, options);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Парсит CSV с использованием Web Workers
|
|
487
|
+
* @param {string|File} csvInput - CSV строка или File объект
|
|
488
|
+
* @param {Object} [options] - Опции парсинга
|
|
489
|
+
* @param {Function} [onProgress] - Callback прогресса
|
|
490
|
+
* @returns {Promise<Array<Object>>} JSON данные
|
|
491
|
+
*/
|
|
492
|
+
export async function parseCSVWithWorker(csvInput, options: any = {}, onProgress = null) {
|
|
493
|
+
// Создание pool если нужно
|
|
494
|
+
const poolHolder = parseCSVWithWorker as any;
|
|
495
|
+
if (!poolHolder.pool) {
|
|
496
|
+
poolHolder.pool = createWorkerPool();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const pool = poolHolder.pool;
|
|
500
|
+
|
|
501
|
+
// Подготовка CSV строки
|
|
502
|
+
// ?????????? CSV ??????
|
|
503
|
+
let csvPayload = csvInput;
|
|
504
|
+
let transfer = null;
|
|
505
|
+
|
|
506
|
+
if (csvInput instanceof File) {
|
|
507
|
+
const buffer = await readFileAsArrayBuffer(csvInput);
|
|
508
|
+
csvPayload = new Uint8Array(buffer);
|
|
509
|
+
transfer = [buffer];
|
|
510
|
+
} else if (csvInput instanceof ArrayBuffer) {
|
|
511
|
+
csvPayload = csvInput;
|
|
512
|
+
transfer = [csvInput];
|
|
513
|
+
} else if (ArrayBuffer.isView(csvInput)) {
|
|
514
|
+
csvPayload = csvInput;
|
|
515
|
+
if (csvInput.buffer instanceof ArrayBuffer) {
|
|
516
|
+
transfer = [csvInput.buffer];
|
|
517
|
+
}
|
|
518
|
+
} else if (typeof csvInput !== 'string') {
|
|
519
|
+
throw new ValidationError('Input must be a CSV string, File, or ArrayBuffer');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ????????? ?????? ????? pool
|
|
523
|
+
const execOptions = transfer ? { transfer } : {};
|
|
524
|
+
return pool.exec('parseCSV', [csvPayload, options], execOptions, onProgress);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Чтение файла как текст
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
async function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
|
532
|
+
return new Promise((resolve, reject) => {
|
|
533
|
+
const reader = new FileReader();
|
|
534
|
+
reader.onload = (event) =>
|
|
535
|
+
resolve((event.target as FileReader).result as ArrayBuffer);
|
|
536
|
+
reader.onerror = (error) => reject(error);
|
|
537
|
+
reader.readAsArrayBuffer(file);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Экспорт для Node.js совместимости
|
|
542
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
543
|
+
module.exports = {
|
|
544
|
+
WorkerPool,
|
|
545
|
+
createWorkerPool,
|
|
546
|
+
parseCSVWithWorker
|
|
547
|
+
};
|
|
548
|
+
}
|
|
@@ -150,28 +150,42 @@ class DelimiterCache {
|
|
|
150
150
|
// Используем первую непустую строку для детектирования
|
|
151
151
|
const firstLine = lines[0];
|
|
152
152
|
|
|
153
|
+
// Быстрый подсчёт вхождений кандидатов за один проход
|
|
153
154
|
const counts = {};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
const candidateSet = new Set(candidates);
|
|
156
|
+
for (let i = 0; i < firstLine.length; i++) {
|
|
157
|
+
const char = firstLine[i];
|
|
158
|
+
if (candidateSet.has(char)) {
|
|
159
|
+
counts[char] = (counts[char] || 0) + 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Убедимся, что все кандидаты присутствуют в counts (даже с нулём)
|
|
163
|
+
for (const delim of candidates) {
|
|
164
|
+
if (!(delim in counts)) {
|
|
165
|
+
counts[delim] = 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
160
168
|
|
|
161
169
|
// Находим разделитель с максимальным количеством
|
|
162
170
|
let maxCount = -1;
|
|
163
171
|
let detectedDelimiter = ';';
|
|
172
|
+
const maxDelimiters = [];
|
|
164
173
|
|
|
165
174
|
for (const [delim, count] of Object.entries(counts)) {
|
|
166
175
|
if (count > maxCount) {
|
|
167
176
|
maxCount = count;
|
|
168
|
-
|
|
177
|
+
maxDelimiters.length = 0;
|
|
178
|
+
maxDelimiters.push(delim);
|
|
179
|
+
} else if (count === maxCount) {
|
|
180
|
+
maxDelimiters.push(delim);
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
// Если разделитель не найден или есть ничья, возвращаем стандартный
|
|
173
|
-
if (maxCount === 0) {
|
|
185
|
+
if (maxCount === 0 || maxDelimiters.length > 1) {
|
|
174
186
|
detectedDelimiter = ';';
|
|
187
|
+
} else {
|
|
188
|
+
detectedDelimiter = maxDelimiters[0];
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
// Сохраняем в кэш если он предоставлен
|