Files
galaxy-game/ui/frontend/src/routes/+layout.svelte
T
Ilia Denisov 11f51944df
Tests · UI / test (push) Successful in 2m19s
Tests · UI / test (pull_request) Successful in 2m25s
fix(ui): register the service worker in production only
SvelteKit's automatic SW registration also runs under `vite dev`, where
the worker intercepted/cached the dev-server e2e suite (42 failures).
Disable auto-registration (kit.serviceWorker.register: false) and
register the worker manually from the root layout guarded by `!dev`, so
`vite dev` and the e2e suite run worker-free while the production build —
and the PWA preview test — still install it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:56:32 +02:00

115 lines
3.3 KiB
Svelte

<script lang="ts">
import "$lib/theme/tokens.css";
import "$lib/theme/base.css";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { dev } from "$app/environment";
import { i18n } from "$lib/i18n/index.svelte";
import { session } from "$lib/session-store.svelte";
import { eventStream } from "../api/events.svelte";
import { loadCore } from "../platform/core/index";
import { GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import ToastHost from "$lib/toast-host.svelte";
let { children } = $props();
// `streamSessionId` records the device session id the event stream
// is currently bound to. The `$effect` below uses it to detect a
// re-login (different session id while still authenticated) and
// restart the stream against the fresh credentials.
let streamSessionId: string | null = null;
onMount(() => {
void session.init();
// Production-only service-worker registration (auto-register is off
// in svelte.config.js) so `vite dev` and the dev-server e2e suite
// run without the worker intercepting requests.
if (!dev && "serviceWorker" in navigator) {
void navigator.serviceWorker.register("/service-worker.js");
}
return () => {
eventStream.stop();
streamSessionId = null;
};
});
$effect(() => {
if (
session.status === "authenticated" &&
session.keypair !== null &&
session.deviceSessionId !== null &&
GATEWAY_RESPONSE_PUBLIC_KEY.length > 0
) {
const keypair = session.keypair;
const deviceSessionId = session.deviceSessionId;
if (streamSessionId !== deviceSessionId) {
if (streamSessionId !== null) {
eventStream.stop();
}
streamSessionId = deviceSessionId;
void (async (): Promise<void> => {
try {
const core = await loadCore();
// Bail out if the session flipped away from this id
// while we were loading core (logout, re-login).
if (
session.deviceSessionId !== deviceSessionId ||
session.status !== "authenticated"
) {
return;
}
eventStream.start({
core,
keypair,
deviceSessionId,
gatewayResponsePublicKey: GATEWAY_RESPONSE_PUBLIC_KEY,
});
} catch (err) {
console.info("layout: failed to start event stream", err);
}
})();
}
} else if (streamSessionId !== null) {
eventStream.stop();
streamSessionId = null;
}
const pathname = page.url.pathname;
// Debug-only routes under /__debug/* run their own bootstrap
// path against the storage primitives and must bypass the
// auth guard so Phase 6's Playwright spec can drive the
// keystore directly.
if (pathname.startsWith("/__debug/")) {
return;
}
if (session.status === "anonymous" && pathname !== "/login") {
void goto("/login", { replaceState: true });
} else if (session.status === "authenticated" && pathname === "/login") {
void goto("/lobby", { replaceState: true });
}
});
</script>
{#if session.status === "loading"}
<main class="status">
<p>{i18n.t("common.loading")}</p>
</main>
{:else if session.status === "unsupported"}
<main class="status">
<h1>{i18n.t("common.browser_not_supported_title")}</h1>
<p>{i18n.t("common.browser_not_supported_body")}</p>
</main>
{:else}
{@render children()}
{/if}
<ToastHost />
<style>
.status {
padding: var(--space-6);
font-family: var(--font-sans);
}
</style>