hvordan øya husker

9. september 2026 meta devlogsveltekit

Fuglene og fisken på denne siden skal føles levende, driver over himmelen og strandlinjestripen i header og footer. Det jeg ikke tenkte gjennom før en leser (meg, som tester min egen side som en normal person for en gangs skyld) påpekte det, er hva som skjer når du trykker oppdater. Hver eneste gang nullstilte hele scenen seg. Andre fugler, andre startposisjoner, som om himmelen ble visket ut og repopulert fra bunnen av i det øyeblikket du så bort. Det føltes ikke levende, det føltes tilfeldig regenerert, som er en helt annen ting.

forespørselen, med mine egne ord

Jeg skrev meg selv en notis den gangen som sa, omtrent, "tilfeldig hvis besøk på under 5min... hvis ikke spor hvor det ble, og hold hvis siden endres." Å klikke rundt på siden skal ikke røre scenen i det hele tatt, siden komponentene monteres én gang fra rot-layoutet og forblir montert hele tiden du navigerer klient-side. Den delen var allerede gratis, SvelteKit remonterer ikke en komponent bare fordi URL-en endret seg under den. Det faktiske gapet var det andre tilfellet: en hard omlasting, eller å åpne en fersk fane. Det er øyeblikket komponenten remonteres fra ingenting, og frem til det punktet betydde "fra ingenting" et helt nytt tilfeldig trekk hver gang.

den første versjonen var ikke feil, den var frossen

Mitt første forsøk på å fikse dette var nesten riktig og helt ødelagt på en måte jeg ikke la merke til på en dag. Jeg lagret den tegnede scenen i localStorage, hvilke sprites som ble valgt og hvilket punkt i flysyklusen hver av dem var på, og gjenopprettet den ved neste montering i stedet for å terne på nytt:

// first attempt: save the phase, restore the exact same phase
function restoreScene(saved: ScenePhases): ScenePhases {
	return saved;
}

Den delen fungerte, i den forstand at de lagrede dataene kom tilbake intakte. Det jeg glemte var tid. Jeg lagret den eksakte fasen hver fugl var i da siden ble avlastet, og ved neste lasting satte jeg dem bare... tilbake akkurat der. Frosset. Hvis du lot fanen ligge i dokken i to minutter og kom tilbake, hadde fuglene ikke beveget seg en tomme, fordi ingenting tok hensyn til de to minuttene som faktisk hadde gått i den virkelige verden.

Den ekte fiksen måtte fremskyve hver lagrede fase med hvor lenge den besøkende faktisk var borte, wrappet riktig rundt hver fugls egen syklustid, slik at en fugl som flyr kant til kant hvert 75. sekund og har vært "borte" i 200 sekunder kommer tilbake midtveis i sin tredje runde, ikke sittende fast på sekund tolv av runde én for alltid:

export function advancePhases(
	phases: ScenePhases,
	cycles: Record<string, number>,
	elapsedMs: number
): ScenePhases {
	const elapsedS = Math.max(0, elapsedMs) / 1000;
	const advanced: ScenePhases = {};
	for (const [key, value] of Object.entries(phases)) {
		const cycle = cycles[key];
		if (!cycle || cycle <= 0 || !Number.isFinite(value)) {
			advanced[key] = value;
			continue;
		}
		advanced[key] = (((value + elapsedS) % cycle) + cycle) % cycle;
	}
	return advanced;
}

Dobbel-moduloen på slutten ((((value + elapsedS) % cycle) + cycle) % cycle) er der fordi JavaScripts % kan returnere et negativt resultat hvis venstre side er negativ, og jeg ville at denne funksjonen skulle være trygg å kalle med en rar eller negativ forløpt tid også (klokkeskjevhet, et lagret tidsstempel fra fremtiden, hva som helst) uten å produsere en fase utenfor sitt eget gyldige område. Belte og bukseseler, men billige sådanne.

foreldethet har også et tak

Ingenting av dette skal holde for alltid. En scene fra seks timer siden er ikke "det samme besøket som fortsetter," det er et nytt besøk som tilfeldigvis deler en nettleser. Så den lagrede scenen bærer sin egen alderssjekk og utløper etter fem minutter:

export const SCENE_MAX_AGE_MS = 5 * 60 * 1000;

Etter det, eller hvis ingenting var lagret, eller hvis de lagrede dataene ikke lar seg parse, terner den bare opp en fersk scene slik den alltid gjorde før noe av dette eksisterte. Sørget for at hver lesing her er wrappet i try/catch også, Safari i privat nettlesing kaster feil bare ved å røre localStorage i det hele tatt, og en besøkende med lagring deaktivert skal ikke få en ødelagt side, de skal bare få den vanlige ferske-terningen-scenen uten noe minne knyttet til den. Å miste persistering stille er greit. Å kaste en feil der en fugl skulle vært er det ikke.

rynken: hva som faktisk teller som "omlasting"

Her er delen som tok lengst tid å få riktig, etter at jeg allerede trodde jeg var ferdig. En besøkende som laster siden på nytt med vilje, trykker den faktiske oppdater-knappen fordi de vil se på siden fersk, forventer at en omlasting stokker ting, samme som et førstebesøk ville gjort. Men gjenopprettingslogikken min kunne ikke skille "du trykket oppdater" fra "du klikket en lenke" innenfra komponenten, fordi når den kjører, vet den bare "en montering skjedde." Begge tilfellene ser bare ut som en montering.

Nettleseren forteller deg faktisk forskjellen, gjennom et API jeg ikke visste eksisterte før jeg lette: performance.getEntriesByType('navigation') returnerer en oppføring hvis type bokstavelig talt er 'reload' for en bevisst omlasting, versus 'navigate' for stort sett alt annet:

export function safeNavigationType(): NavigationType | undefined {
	try {
		if (typeof performance === 'undefined' || typeof performance.getEntriesByType !== 'function') {
			return undefined;
		}
		const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
		return entry?.type;
	} catch {
		return undefined;
	}
}

All denne matematikken, terningen, lagringen, gjenopprettingen, faseframskyvningen, lever i en ren TypeScript-modul helt separat fra Svelte-komponenten som faktisk rendrer fuglene, spesifikt slik at jeg kunne skrive ekte enhetstester mot den uten å trenge en nettleser eller en Svelte-runtime spunnet opp i det hele tatt. Gitt et falskt in-memory-lagringsobjekt og et fast tidsstempel i stedet for den ekte Date.now(), kan jeg bekrefte nøyaktig hva en fem-minutter-gammel lagring gjenoppretter til, hva en seks-minutter-gammel faller tilbake til, og hva en korrupt eller delvis skrevet lagring gjør, alt uten noensinne å røre en faktisk side. Den separasjonen er det som gjorde meg trygg nok til å shippe fremskyvnings-fiksen i utgangspunktet, jeg kunne se fasetallene lande akkurat der matematikken sa de skulle, i stedet for å eyeballe om en fugl "så riktig ut" etter å ha ventet rundt i noen minutter i en ekte nettleserfane.

Og så den faktiske avgjørelsen, som leser nesten kjedelig enkelt når de to bitene ovenfor først finnes for å støtte den:

const saved = navigationType === 'reload' ? null : loadScene<unknown>(storage, SKY_COLONY_STORAGE_KEY, now);

En ekte omlasting hopper over den lagrede scenen helt og terner opp fersk, med vilje, hver gang. Alt annet, tilbakeknapp, en fersk fane, bare navigere rundt, gjenoppretter og fremskyver som normalt. Det er en liten distinksjon og det tok meg lengre å spore opp enn selve persisteringslogikken gjorde, men det er forskjellen mellom at scenen oppfører seg slik en faktisk besøkende forventer og teknisk-korrekt-men-irriterende.

0 kommentarer

Logg inn for å kommentere.

Logg inn

Ingen konto?