I have an Authentication store based on Xstate that I am trying to set the actors based on the config I get from another machine...
export function createAuthMachine(fakeLogin) { const chosenActors = fakeLogin === "true" || fakeLogin === true ? fakeActors : realActors;return setup({ actions, actors: chosenActors }).createMachine(
The other machine (Config Machine) goes to get some global config stuff from the backend...
import { useMachine } from '@xstate/svelte';import { setContext } from 'svelte';import {globalConfigMachine} from "../../lib/config/index.mjs";const { snapshot, send } = useMachine(globalConfigMachine);$: if ($snapshot.matches('done')) { setContext('globalConfig', $snapshot.context.config);}setContext('globalConfigSnapshot', snapshot);export let config = $snapshot.context.config;<slot {config}></slot>
Once this is done I then want to create an AuthMachine that uses the config to setup OAuth. In this case I am using @auth0/auth0-spa-js
import {getContext, onMount, setContext} from "svelte";import {useMachine} from "@xstate/svelte";import {createAuthMachine} from "$lib/auth/index.js";const globalConfigSnapshot = getContext('globalConfigSnapshot');... $: if ($globalConfigSnapshot?.matches) { if ($globalConfigSnapshot.matches('done')) { const config = $globalConfigSnapshot.context.config; if(config == null){ throw new Error("The Config cannot be null when creating the Auth Machine"); } const authMachine = createAuthMachine(config.fakeLogin); authService = useMachine(authMachine, { input: config }); setContext('authService', authService); }}<slot/>
This all seems to work but the problem is I want to have a main page with the following logic...
- If the authService has not been set on the MainPage state is loading.
- If the authService is set wait for the authService state to be idle.
- if the auth service is not set after 30 sec then Main page state should be set to failed
- Once the state is idle the MainPage state should no longer be loading
- if the authstate is Authenticated the MainPage state becomes authenticated
- Otherwise the MainPage state is Unauthenticated
That leaves us with 4 possible main page states
- Loading
- Error
- Authenticated
- Unauthenticated
I tried to accomplish this like
<script> import {getContext, onMount} from "svelte"; import {writable} from "svelte/store"; import {Dashboard} from "../dashboard/index.js"; import {Marketing} from "../marketing/index.js"; const MainState = { LOADING: 'loading', ERROR: 'error', AUTHENTICATED: 'authenticated', UNAUTHENTICATED: 'unauthenticated' }; const authService = writable(null); const MAX_RETRIES = 5; const INITIAL_DELAY = 1000; const hasGivenUp = writable(false); const mainState = writable(MainState.LOADING); const checkAuthService = () => { const service = getContext("authService"); if (service) { authService.set(service); return true; } return false; }; onMount(() => { if (!checkAuthService()) { let retryCount = 0; let delay = INITIAL_DELAY; const attemptConnection = () => { if (retryCount >= MAX_RETRIES) { hasGivenUp.set(true); mainState.set(MainState.ERROR); return; } if (checkAuthService()) return; retryCount++; delay *= 2; setTimeout(attemptConnection, delay); }; attemptConnection(); } }); $: { if ($authSnapshot?.matches('idle')) { mainState.set($authSnapshot.context.isAuthenticated ? MainState.AUTHENTICATED : MainState.UNAUTHENTICATED ); } } // Only access snapshot and send when authService is available $: serviceData = $authService || { snapshot: { subscribe: () => () => { } }, send: () => { } }; $: ({snapshot: authSnapshot, send} = serviceData); const handleLogin = () => { logMain('Login button clicked'); send({type: "LOGIN"}); }; const handleLogout = () => { logMain('Logout button clicked'); send({type: "LOGOUT"}); };</script><section class="flex flex-col min-h-screen bg-marketing"><header class="bg-primary text-white shadow-xl p-4 flex justify-between items-center"><h1 class="text-xl font-bold">Welcome to Love Monkey</h1><nav> {#if $mainState !== MainState.LOADING && $mainState !== MainState.ERROR} {#if $mainState === MainState.UNAUTHENTICATED}<button class="btn btn-sm variant-ghost-surface" on:click={handleLogin}> Login</button> {:else}<button class="btn btn-sm variant-ghost-surface" on:click={handleLogout}> Logout</button> {/if} {/if}</nav></header><main class="flex-grow"> {#if $mainState === MainState.LOADING}<div class="p-4">Loading...</div> {:else if $mainState === MainState.ERROR}<Marketing/> {:else} {#if $mainState === MainState.AUTHENTICATED}<Dashboard/> {:else}<Marketing/> {/if} {/if}</main></section>
But I get
Uncaught Error: https://svelte.dev/e/lifecycle_outside_component at v (Main.svelte:26:25) at P (Main.svelte:47:21)
Of course that tells me that this is not allowed...
onMount(() => { ... if (checkAuthService()) return; << This
and I understand that but I am unsure how to fix it. If I need to wait for something to eventually be on the context how do I delay the rendering until after that happens? Here is the wrapping code
<GlobalConfigProvider><AuthProvider><Main></Main></AuthProvider></GlobalConfigProvider>
#Update
I also tried updating the page to not render till it is available...
<script> import GlobalConfigProvider from "../components/config/GlobalConfigProvider.svelte"; import AuthProvider from "../components/auth/AuthProvider.svelte"; import Main from "./main/Main.svelte"; import { getContext } from "svelte"; import { writable } from "svelte/store"; import { onMount } from "svelte"; const isAuthServiceReady = writable(false); const showFallback = writable(false); let fallbackTimer; const POLLING_INTERVAL = 1000; // Check every 500ms const MAX_ATTEMPTS = 10; // Maximum number of attempts let attempts = 0; function checkAuthService() { const service = getContext("authService"); isAuthServiceReady.set(!!service); } onMount(() => { fallbackTimer = setInterval(() => { console.log('[Page] Checking auth service ready:', $isAuthServiceReady, 'attempt:', attempts); if ($isAuthServiceReady) { clearInterval(fallbackTimer); console.log('[Page] Auth service is ready'); return; } attempts++; if (attempts >= MAX_ATTEMPTS) { clearInterval(fallbackTimer); console.log('[Page] Max attempts reached, showing fallback'); showFallback.set(true); } }, POLLING_INTERVAL); return () => { if (fallbackTimer) clearInterval(fallbackTimer); }; }); $: { checkAuthService(); }</script>
However, that doesn't seem to work the logs I get are
[Auth Machine] Initializing client...AuthProvider.svelte:45 [AuthProvider] Auth machine created and startedindex.js:22 [Auth Machine] Idle...+page.svelte:23 [Page] Checking auth service ready: false attempt: 0+page.svelte:23 [Page] Checking auth service ready: false attempt: 1+page.svelte:23 [Page] Checking auth service ready: false attempt: 2+page.svelte:23 [Page] Checking auth service ready: false attempt: 3+page.svelte:23 [Page] Checking auth service ready: false attempt: 4+page.svelte:23 [Page] Checking auth service ready: false attempt: 5+page.svelte:23 [Page] Checking auth service ready: false attempt: 6+page.svelte:23 [Page] Checking auth service ready: false attempt: 7+page.svelte:23 [Page] Checking auth service ready: false attempt: 8+page.svelte:23 [Page] Checking auth service ready: false attempt: 9+page.svelte:34 [Page] Max attempts reached, showing fallbackMain.svelte:16 [Main Page] Auth snapshot update: "initializeClient"