diff --git a/apps/api/package.json b/apps/api/package.json index e3d1715..b9cc2a3 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -8,12 +8,13 @@ "start": "node src/server.js" }, "dependencies": { - "fastify": "^4.28.1", - "@fastify/rate-limit": "^9.0.0", - "@fastify/helmet": "^11.1.1", + "@fastify/compress": "^7.0.3", "@fastify/cookie": "^9.4.0", - "pg": "^8.12.0", + "@fastify/helmet": "^11.1.1", + "@fastify/rate-limit": "^9.0.0", "bcryptjs": "^2.4.3", - "jsonwebtoken": "^9.0.2" + "fastify": "^4.28.1", + "jsonwebtoken": "^9.0.2", + "pg": "^8.12.0" } } diff --git a/apps/api/src/app.js b/apps/api/src/app.js index c5dd399..7009f7a 100644 --- a/apps/api/src/app.js +++ b/apps/api/src/app.js @@ -7,6 +7,7 @@ */ import Fastify from "fastify"; import rateLimit from "@fastify/rate-limit"; +import compress from "@fastify/compress"; import helmet from "@fastify/helmet"; import cookie from "@fastify/cookie"; import { timingSafeEqual } from "crypto"; @@ -34,6 +35,12 @@ import { ValidationError, sanitizeSearchString, parseLimitOffset } from "./valid * @param {number} [opts.adminRateLimitMax] requêtes/min sur /admin/import * @param {number} [opts.authRateLimitMax] requêtes/min sur /admin/auth/login */ +// Plafonds de /api/clusters. Nommés plutôt qu'écrits en dur dans le SQL : la +// réponse doit pouvoir dire qu'elle a été tronquée, ce qui suppose de comparer +// au même nombre des deux côtés. +const BANDS_AT_ZOOM_LIMIT = 2000; +const CLUSTERS_LIMIT = 1000; + export async function buildServer(opts = {}) { const { pool = null, @@ -60,6 +67,28 @@ export async function buildServer(opts = {}) { trustProxy: true // derrière Traefik (+ nginx pour /admin/*) : lit X-Forwarded-For }); + // Compression. + // + // Le site public charge l'intégralité du corpus une seconde après l'ouverture + // de la page (loadAllHeatPoints), pour la heatmap et la recherche. Mesuré sur + // 100 000 groupes avec des données réalistes : 22 Mo non compressés contre + // 1,9 Mo en gzip. Rien ne compressait dans la chaîne — ni ici, ni Traefik. + // Ce n'était donc pas un risque théorique mais la facture de chaque visiteur, + // à chaque visite. + // + // Seuil à 1 Ko : en dessous, l'en-tête et le temps CPU coûtent plus que ce + // qu'ils économisent. + // + // gzip est préféré à brotli, contre l'intuition : mesuré sur la réponse + // complète de /api/bands, gzip sort 1,9 Mo contre 2,3 Mo pour brotli. La + // qualité brotli par défaut est réglée pour la vitesse, et sur ce JSON très + // répétitif elle perd sur les deux tableaux à la fois — taille ET CPU. + await fastify.register(compress, { + global: true, + threshold: 1024, + encodings: ["gzip", "br", "deflate"], + }); + // Headers de sécurité await fastify.register(helmet, { contentSecurityPolicy: false, @@ -372,14 +401,20 @@ export async function buildServer(opts = {}) { JOIN bands b ON b.ma_id = bl.ma_id WHERE ${whereSql} ORDER BY bl.ma_id ASC, bl.step_order ASC - LIMIT 2000; + LIMIT ${BANDS_AT_ZOOM_LIMIT}; `; const r = await p.query(sql, vals); + // La réponse était plafonnée sans le dire : dans une zone dense, des + // groupes disparaissaient de la carte sans aucun signal, ni côté API ni + // côté client. `truncated` permet enfin à l'interface d'inviter à + // zoomer au lieu de mentir par omission. return { ok: true, type: "bands", items: r.rows, - count: r.rows.length + count: r.rows.length, + limit: BANDS_AT_ZOOM_LIMIT, + truncated: r.rows.length >= BANDS_AT_ZOOM_LIMIT }; } @@ -436,7 +471,7 @@ export async function buildServer(opts = {}) { END as sample_bands FROM clusters ORDER BY band_count DESC - LIMIT 1000; + LIMIT ${CLUSTERS_LIMIT}; `; vals.push(cellSize); @@ -446,6 +481,8 @@ export async function buildServer(opts = {}) { type: "clusters", items: r.rows, total_clusters: r.rows.length, + limit: CLUSTERS_LIMIT, + truncated: r.rows.length >= CLUSTERS_LIMIT, cell_size: cellSize, zoom: zoomLevel }; diff --git a/apps/api/test/publicRoutes.test.js b/apps/api/test/publicRoutes.test.js index b03ca03..c71bd0d 100644 --- a/apps/api/test/publicRoutes.test.js +++ b/apps/api/test/publicRoutes.test.js @@ -58,6 +58,25 @@ describe("/api/health et /", () => { }); }); +describe("/api/clusters — troncature", () => { + it("signale la troncature quand le plafond est atteint", async () => { + // La réponse était plafonnée sans le dire : dans une zone dense, des + // groupes disparaissaient de la carte sans aucun signal. + const pleine = Array.from({ length: 2000 }, (_, i) => ({ ma_id: i })); + const res = await build([{ match: "FROM band_locations", result: { rows: pleine, rowCount: 2000 } }]) + .inject({ method: "GET", url: "/api/clusters?bbox=0,0,10,10&zoom=13" }); + const body = JSON.parse(res.payload); + expect(body.truncated).toBe(true); + expect(body.limit).toBe(2000); + }); + + it("ne signale rien quand tout tient", async () => { + const res = await build([{ match: "FROM band_locations", result: { rows: [{ ma_id: 1 }], rowCount: 1 } }]) + .inject({ method: "GET", url: "/api/clusters?bbox=0,0,10,10&zoom=13" }); + expect(JSON.parse(res.payload).truncated).toBe(false); + }); +}); + describe("CORS", () => { it("renvoie l'en-tête pour une origine autorisée", async () => { const res = await build().inject({ diff --git a/apps/web/site/app.js b/apps/web/site/app.js index d686a18..b3844a9 100644 --- a/apps/web/site/app.js +++ b/apps/web/site/app.js @@ -407,6 +407,18 @@ async function loadViewportBands() { } $("count").textContent = String(j.count || currentViewportData.length || 0); + + // Le serveur plafonne sa réponse (2000 groupes, 1000 clusters). Il le dit + // désormais ; sans cet indicateur, des groupes disparaissaient de la carte + // dans les zones denses sans que rien ne le signale. + const note = $("truncNote"); + if (note) { + note.hidden = !j.truncated; + if (j.truncated) { + note.textContent = "⚠"; + note.title = t("truncated_hint").replace("{n}", String(j.limit ?? "")); + } + } } } catch (e) { console.error("Failed to load viewport bands:", e); diff --git a/apps/web/site/index.html b/apps/web/site/index.html index 77bc599..1fedfde 100644 --- a/apps/web/site/index.html +++ b/apps/web/site/index.html @@ -113,7 +113,7 @@
- 0 affichés + 0 affichés 0 total 0 localités 0 localisés diff --git a/apps/web/site/locales.js b/apps/web/site/locales.js index 1f6294d..c5df4c5 100644 --- a/apps/web/site/locales.js +++ b/apps/web/site/locales.js @@ -57,6 +57,7 @@ fr: { search_placeholder: "Rechercher (nom, genre, ville, pays)…", + truncated_hint: "Seuls les {n} premiers résultats de cette zone sont affichés — zoomez pour tout voir.", displayed: "affichés", total_bands: "total", localities: "localités", located: "localisés", view_section: "Vue", reset_view: "Reset vue", heatmap_label: "Heatmap", filters_section: "Filtres", without_coords_btn: "Sans coords", @@ -95,6 +96,7 @@ en: { search_placeholder: "Search (name, genre, city, country)…", + truncated_hint: "Only the first {n} results in this area are shown — zoom in to see them all.", displayed: "displayed", total_bands: "total", localities: "localities", located: "located", view_section: "View", reset_view: "Reset view", heatmap_label: "Heatmap", filters_section: "Filters", without_coords_btn: "Without coords", @@ -132,6 +134,7 @@ de: { search_placeholder: "Suchen (Name, Genre, Stadt, Land)…", + truncated_hint: "Es werden nur die ersten {n} Ergebnisse in diesem Bereich angezeigt — zoomen Sie hinein, um alle zu sehen.", displayed: "angezeigt", total_bands: "gesamt", localities: "Orte", located: "geortet", view_section: "Ansicht", reset_view: "Ansicht zurücksetzen", heatmap_label: "Heatmap", filters_section: "Filter", without_coords_btn: "Ohne Koordinaten", @@ -169,6 +172,7 @@ es: { search_placeholder: "Buscar (nombre, género, ciudad, país)…", + truncated_hint: "Solo se muestran los primeros {n} resultados de esta zona — amplía para verlos todos.", displayed: "mostrados", total_bands: "total", localities: "localidades", located: "localizados", view_section: "Vista", reset_view: "Restablecer vista", heatmap_label: "Mapa de calor", filters_section: "Filtros", without_coords_btn: "Sin coordenadas", @@ -206,6 +210,7 @@ it: { search_placeholder: "Cerca (nome, genere, città, paese)…", + truncated_hint: "Vengono mostrati solo i primi {n} risultati di quest'area — ingrandisci per vederli tutti.", displayed: "visualizzati", total_bands: "totale", localities: "località", located: "geolocalizzati", view_section: "Vista", reset_view: "Reimposta vista", heatmap_label: "Mappa di calore", filters_section: "Filtri", without_coords_btn: "Senza coordinate", @@ -243,6 +248,7 @@ pl: { search_placeholder: "Szukaj (nazwa, gatunek, miasto, kraj)…", + truncated_hint: "Wyświetlono tylko pierwsze {n} wyników w tym obszarze — przybliż, aby zobaczyć wszystkie.", displayed: "wyświetlono", total_bands: "łącznie", localities: "miejscowości", located: "zlokalizowano", view_section: "Widok", reset_view: "Resetuj widok", heatmap_label: "Mapa cieplna", filters_section: "Filtry", without_coords_btn: "Bez współrzędnych", @@ -280,6 +286,7 @@ nl: { search_placeholder: "Zoeken (naam, genre, stad, land)…", + truncated_hint: "Alleen de eerste {n} resultaten in dit gebied worden getoond — zoom in om ze allemaal te zien.", displayed: "weergegeven", total_bands: "totaal", localities: "locaties", located: "gelokaliseerd", view_section: "Weergave", reset_view: "Weergave herstellen", heatmap_label: "Warmtekaart", filters_section: "Filters", without_coords_btn: "Zonder coördinaten", @@ -317,6 +324,7 @@ ro: { search_placeholder: "Căutare (nume, gen, oraş, țară)…", + truncated_hint: "Sunt afișate doar primele {n} rezultate din această zonă — apropie pentru a le vedea pe toate.", displayed: "afişate", total_bands: "total", localities: "localități", located: "localizate", view_section: "Vedere", reset_view: "Resetare vedere", heatmap_label: "Hartă termică", filters_section: "Filtre", without_coords_btn: "Fără coordonate", @@ -354,6 +362,7 @@ pt: { search_placeholder: "Pesquisar (nome, género, cidade, país)…", + truncated_hint: "Apenas os primeiros {n} resultados desta área são mostrados — amplie para ver todos.", displayed: "exibidos", total_bands: "total", localities: "localidades", located: "localizados", view_section: "Vista", reset_view: "Repor vista", heatmap_label: "Mapa de calor", filters_section: "Filtros", without_coords_btn: "Sem coordenadas", @@ -391,6 +400,7 @@ cs: { search_placeholder: "Hledat (název, žánr, město, země)…", + truncated_hint: "Zobrazeno je pouze prvních {n} výsledků v této oblasti — přibližte pro zobrazení všech.", displayed: "zobrazeno", total_bands: "celkem", localities: "míst", located: "lokalizováno", view_section: "Zobrazení", reset_view: "Obnovit zobrazení", heatmap_label: "Tepelná mapa", filters_section: "Filtry", without_coords_btn: "Bez souřadnic", @@ -428,6 +438,7 @@ sv: { search_placeholder: "Sök (namn, genre, stad, land)…", + truncated_hint: "Endast de första {n} resultaten i det här området visas — zooma in för att se alla.", displayed: "visade", total_bands: "totalt", localities: "orter", located: "lokaliserade", view_section: "Vy", reset_view: "Återställ vy", heatmap_label: "Värmekarta", filters_section: "Filter", without_coords_btn: "Utan koordinater", @@ -465,6 +476,7 @@ hu: { search_placeholder: "Keresés (név, műfaj, város, ország)…", + truncated_hint: "Csak az első {n} találat látható ezen a területen — nagyíts rá az összes megtekintéséhez.", displayed: "megjelenített", total_bands: "összesen", localities: "helységek", located: "lokalizált", view_section: "Nézet", reset_view: "Nézet alaphelyzetbe", heatmap_label: "Hőtérkép", filters_section: "Szűrők", without_coords_btn: "Koordináták nélkül", @@ -502,6 +514,7 @@ da: { search_placeholder: "Søg (navn, genre, by, land)…", + truncated_hint: "Kun de første {n} resultater i dette område vises — zoom ind for at se dem alle.", displayed: "vist", total_bands: "i alt", localities: "lokaliteter", located: "lokaliserede", view_section: "Visning", reset_view: "Nulstil visning", heatmap_label: "Varmekort", filters_section: "Filtre", without_coords_btn: "Uden koordinater", @@ -539,6 +552,7 @@ fi: { search_placeholder: "Haku (nimi, genre, kaupunki, maa)…", + truncated_hint: "Vain tämän alueen {n} ensimmäistä tulosta näytetään — lähennä nähdäksesi kaikki.", displayed: "näytetty", total_bands: "yhteensä", localities: "paikkakunnat", located: "paikannettu", view_section: "Näkymä", reset_view: "Palauta näkymä", heatmap_label: "Lämpökartta", filters_section: "Suodattimet", without_coords_btn: "Ilman koordinaatteja", @@ -576,6 +590,7 @@ sk: { search_placeholder: "Hľadať (názov, žáner, mesto, krajina)…", + truncated_hint: "Zobrazuje sa len prvých {n} výsledkov v tejto oblasti — priblížte pre zobrazenie všetkých.", displayed: "zobrazené", total_bands: "celkom", localities: "miest", located: "lokalizované", view_section: "Zobrazenie", reset_view: "Obnoviť zobrazenie", heatmap_label: "Tepelná mapa", filters_section: "Filtre", without_coords_btn: "Bez súradníc", @@ -613,6 +628,7 @@ hr: { search_placeholder: "Pretraži (ime, žanr, grad, država)…", + truncated_hint: "Prikazano je samo prvih {n} rezultata u ovom području — približite za prikaz svih.", displayed: "prikazano", total_bands: "ukupno", localities: "lokaliteta", located: "locirano", view_section: "Prikaz", reset_view: "Resetiraj prikaz", heatmap_label: "Toplinska karta", filters_section: "Filtri", without_coords_btn: "Bez koordinata", @@ -650,6 +666,7 @@ sl: { search_placeholder: "Iskanje (ime, zvrst, mesto, država)…", + truncated_hint: "Prikazanih je le prvih {n} rezultatov na tem območju — približajte za ogled vseh.", displayed: "prikazano", total_bands: "skupaj", localities: "krajev", located: "lokalizirano", view_section: "Pogled", reset_view: "Ponastavi pogled", heatmap_label: "Toplotna karta", filters_section: "Filtri", without_coords_btn: "Brez koordinat", @@ -687,6 +704,7 @@ lt: { search_placeholder: "Ieškoti (pavadinimas, žanras, miestas, šalis)…", + truncated_hint: "Rodomi tik pirmieji {n} šios srities rezultatai — priartinkite, kad matytumėte visus.", displayed: "rodoma", total_bands: "iš viso", localities: "vietovių", located: "lokalizuota", view_section: "Vaizdas", reset_view: "Atstatyti vaizdą", heatmap_label: "Šilumos žemėlapis", filters_section: "Filtrai", without_coords_btn: "Be koordinačių", @@ -724,6 +742,7 @@ lv: { search_placeholder: "Meklēt (nosaukums, žanrs, pilsēta, valsts)…", + truncated_hint: "Tiek rādīti tikai pirmie {n} rezultāti šajā apgabalā — pietuviniet, lai redzētu visus.", displayed: "rādīts", total_bands: "kopā", localities: "vietu", located: "lokalizēts", view_section: "Skats", reset_view: "Atiestatīt skatu", heatmap_label: "Siltuma karte", filters_section: "Filtri", without_coords_btn: "Bez koordinātām", @@ -761,6 +780,7 @@ et: { search_placeholder: "Otsi (nimi, žanr, linn, riik)…", + truncated_hint: "Kuvatakse ainult selle piirkonna esimesed {n} tulemust — suumi sisse, et näha kõiki.", displayed: "kuvatud", total_bands: "kokku", localities: "asulatest", located: "lokaliseeritud", view_section: "Vaade", reset_view: "Lähtesta vaade", heatmap_label: "Soojuskaart", filters_section: "Filtrid", without_coords_btn: "Ilma koordinaatideta", @@ -798,6 +818,7 @@ nb: { search_placeholder: "Søk (navn, sjanger, by, land)…", + truncated_hint: "Bare de første {n} resultatene i dette området vises — zoom inn for å se alle.", displayed: "vist", total_bands: "totalt", localities: "steder", located: "lokaliserte", view_section: "Visning", reset_view: "Tilbakestill visning", heatmap_label: "Varmekart", filters_section: "Filtre", without_coords_btn: "Uten koordinater", diff --git a/apps/web/site/styles.css b/apps/web/site/styles.css index d633ad4..a5d505c 100644 --- a/apps/web/site/styles.css +++ b/apps/web/site/styles.css @@ -1398,4 +1398,14 @@ a:hover { #mapWrap { min-height: 300px; } -} \ No newline at end of file +} + +/* Signale que la carte ne montre qu'une partie des résultats de la zone. + Sans ça, la troncature du serveur était totalement invisible. */ +.trunc-note { + margin-left: 6px; + color: #e0a144; + cursor: help; + font-weight: 700; +} +.trunc-note[hidden] { display: none; } diff --git a/package-lock.json b/package-lock.json index 1eeb6e4..47d4621 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "name": "bm-api", "version": "0.0.1", "dependencies": { + "@fastify/compress": "^7.0.3", "@fastify/cookie": "^9.4.0", "@fastify/helmet": "^11.1.1", "@fastify/rate-limit": "^9.0.0", @@ -40,6 +41,31 @@ "pg": "^8.12.0" } }, + "apps/api/node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "apps/api/node_modules/@fastify/compress": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-7.0.3.tgz", + "integrity": "sha512-xa9fo5/DgK1s0bkS6xrYgNn8HmofO5tJvbCDk8QuXshSgLd2cFZANv1ox/Qv7zswS7JroHwTlCVv/XGTVO98tg==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^1.1.0", + "fastify-plugin": "^4.5.0", + "mime-db": "^1.52.0", + "minipass": "^7.0.2", + "peek-stream": "^1.1.3", + "pump": "^3.0.0", + "pumpify": "^2.0.1", + "readable-stream": "^4.5.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2637,6 +2663,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -2824,6 +2862,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.11.15", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", @@ -2901,12 +2959,42 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3111,6 +3199,12 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3246,6 +3340,48 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3276,6 +3412,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -3636,6 +3781,24 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.1.tgz", @@ -4276,6 +4439,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4317,7 +4500,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ipaddr.js": { @@ -4408,6 +4590,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4837,7 +5025,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4883,7 +5070,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -5038,6 +5224,15 @@ "node": ">=14.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5205,6 +5400,17 @@ "node": ">= 14.16" } }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "node_modules/pg": { "version": "8.23.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", @@ -5495,6 +5701,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", @@ -5524,6 +5745,53 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5574,6 +5842,22 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -5943,6 +6227,21 @@ "dev": true, "license": "MIT" }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -6156,6 +6455,46 @@ "real-require": "^0.2.0" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6430,6 +6769,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -6781,6 +7126,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",