import type db from '@adonisjs/lucid/services/db' type Client = Pick /** Posters are kept to this length (as the trigram index and poster search always did; a btree entry holds 2,704 bytes). */ export const POSTER_MAX = 200 /** Cached ids per process: group sets are ~2,600 in all; posters many more, so the cache is emptied when it grows past this. */ const CACHE_MAX = 200_000 const posterCache = new Map() const groupSetCache = new Map() /** A group list as group_sets stores it: sorted, without repeats, as JSON. */ export function groupSetKey(groups: string[]): string { return JSON.stringify([...new Set(groups)].sort()) } /** A poster as posters stores it. */ export function posterText(poster: string): string { return poster.slice(0, POSTER_MAX) } async function ids( client: Client, cache: Map, table: 'posters' | 'group_sets', column: 'poster' | 'groups', cast: 'text' | 'jsonb', values: string[] ): Promise> { const out = new Map() const missing: string[] = [] for (const v of values) { const id = cache.get(v) if (id === undefined) missing.push(v) else out.set(v, id) } if (missing.length === 0) return out // Insert what's new, then read every id (ON CONFLICT DO NOTHING returns nothing for the rows that existed). await client.rawQuery( `INSERT INTO ${table} (${column}) SELECT DISTINCT v::${cast} FROM unnest(?::text[]) AS v ON CONFLICT DO NOTHING`, [missing] ) const { rows } = await client.rawQuery( `SELECT id, ${column}::text AS value FROM ${table} WHERE ${column} = ANY(?::${cast}[])`, [missing] ) if (cache.size + rows.length > CACHE_MAX) cache.clear() for (const r of rows) { // jsonb comes back in Postgres' spelling ("["a", "b"]"); the caller's key is the JSON we sent. const key = cast === 'jsonb' ? JSON.stringify(JSON.parse(r.value)) : r.value cache.set(key, Number(r.id)) out.set(key, Number(r.id)) } return out } /** * Ids in the catalog's lookup tables (voyager 3, migration 6000): a release's poster_id and group_set_id. New * values are inserted; known ones come from a cache per process (the batch store sees the same posters and * group sets batch after batch). */ export default { /** Ids by poster (posterText applied), for a batch of posters. */ async posterIds(client: Client, posters: string[]): Promise> { return ids(client, posterCache, 'posters', 'poster', 'text', [ ...new Set(posters.map(posterText)), ]) }, async posterId(client: Client, poster: string): Promise { const found = await this.posterIds(client, [poster]) return found.get(posterText(poster))! }, /** Ids by groupSetKey, for a batch of group lists. */ async groupSetIds(client: Client, sets: string[][]): Promise> { return ids(client, groupSetCache, 'group_sets', 'groups', 'jsonb', [ ...new Set(sets.map(groupSetKey)), ]) }, async groupSetId(client: Client, groups: string[]): Promise { const found = await this.groupSetIds(client, [groups]) return found.get(groupSetKey(groups))! }, /** Forgets the cached ids (tests roll their transactions back, so the ids they saw are gone). */ forget() { posterCache.clear() groupSetCache.clear() }, }