Een dating-app op basis van brieven, niet van gezichten!

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

3.8 kB Ruw bestand
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);
}