import { toTerms, countSearchTerms } from "./queries.js"; /* * Waar op gezocht wordt, geteld per woord en per dag — en dus zonder wie het zocht. De tabel en het * waarom staan in db/schema.sql bij `search_terms`; dit bestand lost het ene probleem op dat ertussen * zit: de app zoekt tijdens het typen. * * De client debounced 200 ms (useAsync in src/hooks/useAsync.js), maar dat plakt alleen aanslagen * binnen 200 ms aan elkaar. Wie "dostojevski" typt stuurt nog altijd `do`, `dost`, `dostoje`, * `dostojevski`. Ongefilterd tellen zou dus vooral meten hoe snel iemand typt, en zou de eerste twee * letters van elk woord bovenaan de ranglijst zetten. * * Wat hier gebeurt is daarom geen tijd-debounce maar prefix-collapsing: van elke gebruiker blijft de * laatste zoekopdracht even hangen, en die wordt pas geteld als * - er iets komt dat géén voortzetting is van wat er stond (ander woord → de vorige was af), of * - het IDLE_MS stil blijft (uitgetypt en aan het lezen), of * - het zoekveld leeg wordt gemaakt (ook dat betekent: de vorige was af). * "Voortzetting" is symmetrisch: `dostoje` → `dostojevski` is doortypen, `dostojevski` → `dostoje` * is een backspace. In beide gevallen wint de laatste, want dat is wat er op het scherm staat als * het zoeken stopt. * * Wat er in het geheugen ligt is dus hoogstens één zoekopdracht per ingelogde gebruiker, een paar * seconden lang, en het overleeft geen herstart. Bij een herstart gaan die paar tellingen verloren; * dat is de juiste kant om te verliezen. */ /* Zo lang stilte betekent: hij is uitgetypt */ const IDLE_MS = 4000; /* * Een dak op wat er tegelijk kan hangen. Normaal is dit het aantal mensen dat op ditzelfde moment * aan het typen is (klein), maar een aanhoudende stroom zoekopdrachten van veel accounten mag geen * geheugen laten oplopen; is het dak bereikt, dan gaat alles wat er ligt meteen de teller in. */ const MAX_PENDING = 5000; /* meId → { q, terms, timer } */ const pending = new Map(); const normalize = (q) => q.trim().toLowerCase().replace(/\s+/g, " "); /* Is `next` doorgetypt op `prev`, of teruggehaald? Dan is het dezelfde zoekopdracht */ const zelfdeZoekopdracht = (prev, next) => prev.startsWith(next) || next.startsWith(prev); /* * Tellen mag nooit een zoekopdracht ophouden of laten mislukken: dit is een telling, geen functie. * Vandaar dat de fout hier eindigt en niet doorgegeven wordt. */ const tel = (terms) => countSearchTerms(terms).catch((e) => console.error("zoektermen tellen mislukt:", e.message)); const flush = (meId) => { const held = pending.get(meId); if (!held) return; clearTimeout(held.timer); pending.delete(meId); tel(held.terms); }; /* * Eén zoekopdracht aanmelden. Geeft niets terug en wordt niet afgewacht — de aanroeper is een * verzoek dat op zijn antwoord wacht, en dat antwoord hangt hier niet van af. */ export function noteSearch(meId, q) { const genormaliseerd = normalize(q ?? ""); const terms = toTerms(genormaliseerd); /* Leeg zoekveld: er valt zelf niets te tellen, maar wat er lag was blijkbaar af */ if (!terms.length) return void flush(meId); const held = pending.get(meId); if (held) { clearTimeout(held.timer); /* Iets heel anders? Dan was wat er lag een afgeronde zoekopdracht */ if (!zelfdeZoekopdracht(held.q, genormaliseerd)) tel(held.terms); } const timer = setTimeout(() => { pending.delete(meId); tel(terms); }, IDLE_MS); timer.unref?.(); // een wachtende teller mag het afsluiten van de server niet tegenhouden pending.set(meId, { q: genormaliseerd, terms, timer }); if (pending.size > MAX_PENDING) flushSearchLog(); } /* Alles wat er ligt nu tellen. Voor het dak hierboven, en voor tests die niet willen wachten. */ export function flushSearchLog() { for (const meId of [...pending.keys()]) flush(meId); }