Switched to per-day zipped CSVs with combined CSV included
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onDestroy } from 'svelte';
|
||||
import { getDayRange, formatDayParam } from '$lib/timezone.js';
|
||||
import { fetchDay } from '$lib/api.js';
|
||||
import { downloadCsv, buildFilename } from '$lib/csv.js';
|
||||
import { buildCsv, downloadBlob, buildDayFilename, buildZipFilename } from '$lib/csv.js';
|
||||
import { enqueue } from '$lib/download-pool.js';
|
||||
|
||||
let { station } = $props();
|
||||
@@ -28,9 +28,7 @@
|
||||
let pct = $derived(totalDays > 0 ? (currentDay / totalDays) * 100 : 0);
|
||||
|
||||
async function startDownload() {
|
||||
const { buildCsvAsync } = await import('$lib/csv-pool.js');
|
||||
// Un controller per sessione: abort() cancella tutte le fetch di questa centralina
|
||||
// senza interferire con i download paralleli di altre stazioni.
|
||||
const JSZip = (await import('jszip')).default;
|
||||
controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
downloading = true;
|
||||
@@ -39,8 +37,6 @@
|
||||
totalDays = days.length;
|
||||
currentDay = 0;
|
||||
|
||||
// Array pre-allocato per indice: le fetch partono in parallelo e completano fuori ordine,
|
||||
// ma resultsByDay[idx] garantisce che il CSV rispetti la sequenza temporale.
|
||||
const resultsByDay = new Array(days.length);
|
||||
|
||||
await Promise.all(days.map((day, idx) =>
|
||||
@@ -52,17 +48,27 @@
|
||||
));
|
||||
|
||||
if (!signal.aborted) {
|
||||
const measurements = resultsByDay.flat();
|
||||
currentDay = totalDays;
|
||||
// Breve pausa prima del reset: l'utente deve poter vedere la barra al 100%.
|
||||
await new Promise(r => setTimeout(r, 900));
|
||||
|
||||
if (!signal.aborted && measurements.length > 0) {
|
||||
const csv = await buildCsvAsync(measurements);
|
||||
downloadCsv(csv, buildFilename(station.title, station.pk, new Date()));
|
||||
completed = true;
|
||||
// Il segno di spunta deve restare visibile abbastanza a lungo da essere notato.
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
if (!signal.aborted) {
|
||||
const zip = new JSZip();
|
||||
let allMeasurements = [];
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const measurements = resultsByDay[i];
|
||||
if (!measurements?.length) continue;
|
||||
zip.file(buildDayFilename(days[i]), buildCsv(measurements, false));
|
||||
allMeasurements = allMeasurements.concat(measurements);
|
||||
}
|
||||
|
||||
if (allMeasurements.length > 0) {
|
||||
zip.file(buildZipFilename(station.title, station.pk).replace('.zip', '.csv'), buildCsv(allMeasurements));
|
||||
const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
|
||||
downloadBlob(blob, buildZipFilename(station.title, station.pk));
|
||||
completed = true;
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import CsvWorker from './csv.worker.js?worker';
|
||||
|
||||
// Costruire il CSV di un'intera centralina (potenzialmente decine di migliaia di righe)
|
||||
// blocca il main thread per centinaia di ms: si delega a Worker separati.
|
||||
// Due worker perché la costruzione CSV è CPU-bound: aggiungerne di più non migliora
|
||||
// il singolo job ma aumenta il consumo di memoria.
|
||||
const POOL_SIZE = 2;
|
||||
|
||||
const queue = [];
|
||||
const idle = [];
|
||||
|
||||
for (let i = 0; i < POOL_SIZE; i++) {
|
||||
const w = new CsvWorker();
|
||||
|
||||
w.onmessage = ({ data }) => {
|
||||
// _task aggancia la Promise al Worker: il message handler non riceve l'identità
|
||||
// del worker mittente, quindi è l'unico modo per risalire alla Promise da risolvere.
|
||||
const { resolve } = w._task;
|
||||
w._task = null;
|
||||
idle.push(w);
|
||||
resolve(data);
|
||||
dispatch();
|
||||
};
|
||||
|
||||
w.onerror = e => {
|
||||
const { reject } = w._task;
|
||||
w._task = null;
|
||||
idle.push(w);
|
||||
reject(e);
|
||||
dispatch();
|
||||
};
|
||||
|
||||
idle.push(w);
|
||||
}
|
||||
|
||||
function dispatch() {
|
||||
if (!queue.length || !idle.length) return;
|
||||
const w = idle.pop();
|
||||
w._task = queue.shift();
|
||||
w.postMessage(w._task.measurements);
|
||||
}
|
||||
|
||||
export function buildCsvAsync(measurements) {
|
||||
return new Promise((resolve, reject) => {
|
||||
queue.push({ measurements, resolve, reject });
|
||||
dispatch();
|
||||
});
|
||||
}
|
||||
+14
-12
@@ -1,31 +1,33 @@
|
||||
import { formatTimestampRome, ROME } from './timezone.js';
|
||||
|
||||
export function buildCsv(measurements) {
|
||||
const header = '"Data","Ora","Temperatura","Umidità","PM10"';
|
||||
export function buildCsv(measurements, includeDate = true) {
|
||||
const header = includeDate
|
||||
? '"Data","Ora","Temperatura","Umidità","PM10"'
|
||||
: '"Ora","Temperatura","Umidità","PM10"';
|
||||
const rows = measurements.map(m => {
|
||||
const [date, time] = formatTimestampRome(m.time).split(' ');
|
||||
return `"${date}","${time.split('.')[0]}",${m.temp},${m.umid},${m.pm10}`;
|
||||
const t = time.split('.')[0];
|
||||
return includeDate ? `"${date}","${t}",${m.temp},${m.umid},${m.pm10}` : `"${t}",${m.temp},${m.umid},${m.pm10}`;
|
||||
});
|
||||
// BOM obbligatorio: Excel su Windows interpreta UTF-8 senza BOM come Windows-1252,
|
||||
// corrompendo i caratteri accentati nelle intestazioni.
|
||||
return '' + [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
export function downloadCsv(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
|
||||
export function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
// revokeObjectURL libera subito la memoria del blob: il click sincrono è sufficiente
|
||||
// perché il browser avvia il download prima di passare allo statement successivo.
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function buildFilename(title, id, date) {
|
||||
// en-CA produce YYYY-MM-DD, il formato ISO che vogliamo nel nome file.
|
||||
const dateStr = new Intl.DateTimeFormat('en-CA', { timeZone: ROME }).format(date);
|
||||
const sanitized = title.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
|
||||
return `${sanitized}_(${id})_${dateStr}.csv`;
|
||||
export function buildDayFilename(date) {
|
||||
return new Intl.DateTimeFormat('en-CA', { timeZone: ROME }).format(date) + '.csv';
|
||||
}
|
||||
|
||||
export function buildZipFilename(title, id) {
|
||||
const sanitized = title.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
|
||||
return `${sanitized}_(${id}).zip`;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { buildCsv } from './csv.js';
|
||||
|
||||
self.onmessage = ({ data: measurements }) => {
|
||||
self.postMessage(buildCsv(measurements));
|
||||
};
|
||||
Reference in New Issue
Block a user