Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
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);
}