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",