mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 10:32:10 +00:00
perf(pokedex): send the box grid as packed rows with the page
The server load fetched a full combined-data page at a 9999 item page size, carrying detail text and ownership fields the grid never renders, and the client re-fetched the same payload after hydration. Load a trimmed grid row instead and pack it as positional tuples so field names are not repeated for every one of a thousand-plus entries. Entry detail is fetched on demand from the new per-entry endpoint when a cell is opened, and the grid marks itself interactive so other page-start work can queue behind it.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { packGrid, unpackGrid } from '$lib/models/PokedexGridRow';
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||
import { beforeAll, afterAll, describe, expect, it } from 'vitest';
|
||||
import { requireLoopbackUrl } from '../support/loopback';
|
||||
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
|
||||
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
|
||||
import { loadPokedexGrid, loadPokedexEntryDetail } from '$lib/services/PokedexGridService';
|
||||
import type { Pokedex } from '$lib/models/Pokedex';
|
||||
|
||||
const url = requireLoopbackUrl(
|
||||
process.env.TEST_SUPABASE_URL ?? 'http://127.0.0.1:54321',
|
||||
'TEST_SUPABASE_URL'
|
||||
);
|
||||
const serviceKey = process.env.E2E_SERVICE_ROLE_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const anonKey = process.env.TEST_SUPABASE_ANON_KEY;
|
||||
|
||||
describe('compact Pokédex and partial catch writes', () => {
|
||||
let admin: SupabaseClient;
|
||||
let client: SupabaseClient;
|
||||
let owner = '';
|
||||
let national: Pokedex;
|
||||
let scoped: Pokedex;
|
||||
let firstId: string;
|
||||
let secondId: string;
|
||||
beforeAll(async () => {
|
||||
if (!serviceKey || !anonKey) throw new Error('Use npm run test:integration');
|
||||
admin = createClient(url, serviceKey, { auth: { persistSession: false } });
|
||||
const email = `grid-${crypto.randomUUID()}@example.test`;
|
||||
const password = crypto.randomUUID();
|
||||
const created = await admin.auth.admin.createUser({ email, password, email_confirm: true });
|
||||
if (created.error) throw created.error;
|
||||
owner = created.data.user.id;
|
||||
client = createClient(url, anonKey, { auth: { persistSession: false } });
|
||||
const signed = await client.auth.signInWithPassword({ email, password });
|
||||
if (signed.error) throw signed.error;
|
||||
const repo = new PokedexRepository(client, owner);
|
||||
national = await repo.create({ name: 'Grid national', isFormDex: false });
|
||||
scoped = await repo.create({ name: 'Grid forms', isFormDex: true, gameScope: 'Scarlet' });
|
||||
const links = await client
|
||||
.from('pokedex_dex_scopes')
|
||||
.insert({ pokedexId: scoped._id, dexId: 'scarlet-paldea' });
|
||||
if (links.error) throw links.error;
|
||||
scoped = (await repo.findById(scoped._id))!;
|
||||
const grid = await loadPokedexGrid(client, owner, national);
|
||||
[firstId, secondId] = grid.slice(0, 2).map((row) => row.pokedexEntry._id);
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (owner) await admin.auth.admin.deleteUser(owner);
|
||||
});
|
||||
|
||||
it('loads saved scopes with ownership and prevents cross-account reads', async () => {
|
||||
expect(scoped.dexScopes).toEqual(['scarlet-paldea']);
|
||||
const other = new PokedexRepository(client, crypto.randomUUID());
|
||||
expect(await other.findById(scoped._id)).toBeNull();
|
||||
});
|
||||
|
||||
it('matches full ordering and statuses while reducing serialized rows by at least 60%', async () => {
|
||||
for (const dex of [national, scoped]) {
|
||||
const initial = await loadPokedexGrid(client, owner, dex);
|
||||
const catches = new CatchRecordRepository(client, owner, dex._id);
|
||||
for (let offset = 0; offset < initial.length - 1; offset += 500)
|
||||
await catches.bulkUpsert(
|
||||
initial.slice(offset, Math.min(offset + 500, initial.length - 1)).map((row, index) => ({
|
||||
pokemonId: row.pokedexEntry._id,
|
||||
caught: index % 3 === 0,
|
||||
inHome: index % 7 === 0,
|
||||
personalNotes: ''
|
||||
}))
|
||||
);
|
||||
const grid = await loadPokedexGrid(client, owner, dex);
|
||||
const full = await new CombinedDataRepository(client, owner, dex._id).findAllCombinedData(
|
||||
owner,
|
||||
dex.isFormDex,
|
||||
'',
|
||||
dex.gameScope || '',
|
||||
dex.dexScopes
|
||||
);
|
||||
expect(grid.map((row) => row.pokedexEntry._id)).toEqual(
|
||||
full.map((row) => row.pokedexEntry._id)
|
||||
);
|
||||
expect(grid.length).toBeGreaterThan(400);
|
||||
expect(unpackGrid(packGrid(grid))).toEqual(grid);
|
||||
expect(JSON.stringify(packGrid(grid)).length).toBeLessThan(JSON.stringify(full).length * 0.4);
|
||||
expect(JSON.stringify(grid)).not.toContain('personalNotes');
|
||||
expect(JSON.stringify(grid)).not.toContain('catchInformation');
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves omitted notes and statuses in mixed partial bulk writes, including new records', async () => {
|
||||
const repo = new CatchRecordRepository(client, owner, national._id);
|
||||
await repo.bulkUpsert([
|
||||
{ pokemonId: firstId, personalNotes: 'Keep this note', caught: true, inHome: true }
|
||||
]);
|
||||
await repo.bulkUpsert([
|
||||
{ pokemonId: firstId, haveToEvolve: true, caught: false },
|
||||
{ pokemonId: secondId, personalNotes: 'New record' }
|
||||
]);
|
||||
expect(await repo.findByUserAndPokemon(owner, firstId, national._id)).toMatchObject({
|
||||
personalNotes: 'Keep this note',
|
||||
caught: false,
|
||||
haveToEvolve: true,
|
||||
inHome: true
|
||||
});
|
||||
expect(await repo.findByUserAndPokemon(owner, secondId, national._id)).toMatchObject({
|
||||
personalNotes: 'New record',
|
||||
caught: false
|
||||
});
|
||||
await repo.bulkUpsert([{ pokemonId: firstId, personalNotes: '' }]);
|
||||
expect(await repo.findByUserAndPokemon(owner, firstId, national._id)).toMatchObject({
|
||||
personalNotes: '',
|
||||
inHome: true,
|
||||
haveToEvolve: true
|
||||
});
|
||||
});
|
||||
|
||||
it('returns full details only for members of this dex', async () => {
|
||||
const detail = await loadPokedexEntryDetail(client, owner, national, Number(firstId));
|
||||
expect(detail?.pokedexEntry).toHaveProperty('catchInformation');
|
||||
expect(detail?.catchRecord).toHaveProperty('personalNotes');
|
||||
expect(await loadPokedexEntryDetail(client, owner, national, 999999)).toBeNull();
|
||||
const forms = await loadPokedexGrid(client, owner, scoped);
|
||||
const base = new Set(
|
||||
(await loadPokedexGrid(client, owner, national)).map((row) => row.pokedexEntry._id)
|
||||
);
|
||||
const formOnly = forms.find((row) => !base.has(row.pokedexEntry._id));
|
||||
expect(formOnly).toBeDefined();
|
||||
expect(
|
||||
await loadPokedexEntryDetail(client, owner, national, Number(formOnly!.pokedexEntry._id))
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user