From 099c937eb34ab6f5c1bff42c5a1cc2afdb3c5cff Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sat, 15 Aug 2026 18:25:32 -0500 Subject: [PATCH 01/20] tag the queries that actually make nw reqs update queries.ts creator to pass tags --- src/lib/data/api/aggie_spirit.ts | 9 +++++++++ src/lib/data/api/route_planning.ts | 2 ++ src/lib/utils/queries.ts | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/data/api/aggie_spirit.ts b/src/lib/data/api/aggie_spirit.ts index 563f569..fc25308 100644 --- a/src/lib/data/api/aggie_spirit.ts +++ b/src/lib/data/api/aggie_spirit.ts @@ -45,6 +45,7 @@ export const useAuthCodeAPI = () => { const query = createLoggingQuery(() => ({ label: ASAPIQueryKey.AUTH_CODE, queryKey: [ASAPIQueryKey.AUTH_CODE], + meta: { network: true, auth: true }, queryFn: async () => { const authCodeB64 = await (await fetch('https://auth.maroonrides.app')).text(); return atob(authCodeB64); @@ -60,6 +61,7 @@ export const useAuthTokenAPI = () => { const query = createDependencyQuery(() => ({ queryKey: [ASAPIQueryKey.AUTH_TOKEN], + meta: { network: true }, queryFn: async () => { var res = await fetch('https://aggiespirit.ts.tamu.edu/', { credentials: 'omit' }); @@ -84,6 +86,7 @@ export const useRoutePlanAuthTokenAPI = (params: () => { queryString: string }) const { queryString } = params(); return { queryKey: [ASAPIQueryKey.ROUTE_PLAN_AUTH_TOKEN], + meta: { network: true }, queryFn: async () => { var res = await fetch(`https://aggiespirit.ts.tamu.edu/TripPlanner/${queryString}`, { credentials: 'omit', @@ -111,6 +114,7 @@ export const useBaseDataAPI = () => { const query = createDependencyQuery(() => ({ queryKey: [ASAPIQueryKey.BASE_DATA], + meta: { network: true }, queryFn: async () => { const baseData = await getBaseData(authTokenQuery.data!); GetBaseDataResponseSchema.parse(baseData); @@ -130,6 +134,7 @@ export const usePatternPathsAPI = () => { const query = createDependencyQuery(() => ({ queryKey: [ASAPIQueryKey.PATTERN_PATHS], + meta: { network: true }, queryFn: async () => { const baseData = baseDataQuery.data as IGetBaseDataResponse; @@ -174,6 +179,7 @@ export const useStopEstimateAPI = ( const { routeKey, directionKey, stopCode } = params(); return { queryKey: [ASAPIQueryKey.STOP_ESTIMATE, routeKey, directionKey, stopCode], + meta: { network: true }, queryFn: async () => { const response = await getNextDepartureTimes( routeKey, @@ -200,6 +206,7 @@ export const useTimetableEstimateAPI = (params: () => { stopCode: string; date: const { stopCode, date } = params(); return { queryKey: [ASAPIQueryKey.TIMETABLE_ESTIMATE, stopCode, moment(date).format('YYYY-MM-DD')], + meta: { network: true }, queryFn: async () => { const response = await getStopEstimates(stopCode, date, authTokenQuery.data!); GetStopEstimatesResponseSchema.parse(response); @@ -221,6 +228,7 @@ export const useStopScheduleAPI = (params: () => { stopCode: string; date: Date const { stopCode, date } = params(); return { queryKey: [ASAPIQueryKey.STOP_SCHEDULE, stopCode, moment(date).format('YYYY-MM-DD')], + meta: { network: true }, queryFn: async () => { const response = await getStopSchedules(stopCode, date, authTokenQuery.data!); GetStopSchedulesResponseSchema.parse(response); @@ -242,6 +250,7 @@ export const useVehiclesAPI = (params: () => { routeKey: string }) => { const { routeKey } = params(); return { queryKey: [ASAPIQueryKey.VEHICLES, routeKey], + meta: { network: true }, queryFn: async () => { let busesResponse = (await getVehicles( [routeKey], diff --git a/src/lib/data/api/route_planning.ts b/src/lib/data/api/route_planning.ts index 53b166e..4b82bba 100644 --- a/src/lib/data/api/route_planning.ts +++ b/src/lib/data/api/route_planning.ts @@ -24,6 +24,7 @@ export const useSearchSuggestionAPI = (params: () => { query: string }) => { const { query } = params(); return { queryKey: [ASRoutePlanQueryKey.SEARCH_SUGGESTION, query], + meta: { network: true }, queryFn: async () => { // we need data from pattern paths to get the stop GPS locations // This is limitation of the API where we can't get the GPS location of a stop directly @@ -93,6 +94,7 @@ export const useTripPlanAPI = ( const { origin, destination, date, deadline } = params(); return { queryKey: [ASRoutePlanQueryKey.TRIP_PLAN, origin, destination, date, deadline], + meta: { network: true }, queryFn: async () => { let response = await getTripPlan( routePlanAuthToken.data!, diff --git a/src/lib/utils/queries.ts b/src/lib/utils/queries.ts index 19d116e..169c87d 100644 --- a/src/lib/utils/queries.ts +++ b/src/lib/utils/queries.ts @@ -8,6 +8,7 @@ interface Params { staleTime?: moment.Duration | number; refetchInterval?: moment.Duration | number; enabled?: boolean; + meta?: Record; } interface DependencyQueryParams extends Params { @@ -27,9 +28,9 @@ export function createDependencyQuery(params: () => DependencyQueryParams) const p = params(); const label = p.queryKey.join('/'); const enabled = p.dependents.every((q) => q.isSuccess); - return { queryKey: [...p.queryKey, ...p.dependents.map((q) => q.data)], + meta: p.meta, queryFn: async () => { try { const start = moment.now(); @@ -95,6 +96,7 @@ export function createLoggingQuery(params: () => LoggingQueryParams) { return { queryKey: p.queryKey, + meta: p.meta, queryFn: async () => { try { const start = moment.now(); From 4a193ddca52331282dc7674752211d95e72cc239 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sat, 15 Aug 2026 18:25:59 -0500 Subject: [PATCH 02/20] basic state manager and popup --- .../managers/connectivity.manager.svelte.ts | 33 ++++++++++++++ src/routes/+layout.svelte | 45 +++++++++++++++++-- 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/lib/managers/connectivity.manager.svelte.ts diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts new file mode 100644 index 0000000..8db6521 --- /dev/null +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -0,0 +1,33 @@ +// failing a request n times +const ERR_THRESHOLD = 1; +class ConnectivityManager { + apiError = $state(undefined); + authError = $state(undefined); + + reportError(error: Error) { + this.apiError = true; + } + reportSuccess(data: unknown) { + this.apiError = false; + } + reportAuthError(error: Error) { + this.authError = true; + } + reportAuthSuccess(data: unknown) { + this.authError = false; + } +} + +// Persist connectivityManager across Svelte HMRs +let connectivityManager: ConnectivityManager; + +if (import.meta.hot && import.meta.hot.data) { + if (!import.meta.hot.data.connectivityManager) { + import.meta.hot.data.connectivityManager = new ConnectivityManager(); + } + connectivityManager = import.meta.hot.data.connectivityManager; +} else { + connectivityManager = new ConnectivityManager(); +} + +export { connectivityManager }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 672a071..455a18b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -8,14 +8,32 @@ import { mapManager } from '$lib/managers/map.manager.svelte'; import { installInterceptor } from '$lib/utils/interceptor'; import { migratePrefs } from '$lib/utils/prefs'; - import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; + import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; import { onDestroy, onMount } from 'svelte'; import './layout.css'; + import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; + import { Cloud, CloudOff } from '@lucide/svelte'; + import * as Card from '$lib/components/ui/card'; + import { fly } from 'svelte/transition'; let { children } = $props(); - const queryClient = new QueryClient(); - + const queryClient = new QueryClient({ + defaultOptions: { queries: { networkMode: 'offlineFirst' } }, + queryCache: new QueryCache({ + onError: (error, query) => { + if (query.meta?.network) connectivityManager.reportError(error); + if (query.meta?.auth) connectivityManager.reportAuthError(error); + }, + onSuccess: (data, query) => { + if (query.meta?.network) connectivityManager.reportSuccess(data); + if (query.meta?.auth) connectivityManager.reportAuthSuccess(data); + }, + }), + }); + $effect(() => { + console.log(connectivityManager.apiError, connectivityManager.authError); + }); onDestroy(() => { mapManager.unregisterMap(); }); @@ -33,6 +51,27 @@ + {#if connectivityManager.apiError || connectivityManager.authError} +
+ +
+ +

Offline

+
+ +
+
+
+
+
+ {/if}
Date: Sat, 15 Aug 2026 19:05:45 -0500 Subject: [PATCH 03/20] another style (w Empty) --- .../managers/connectivity.manager.svelte.ts | 25 ++++++++++------- src/routes/+layout.svelte | 27 +++++++------------ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 8db6521..cf23752 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -1,20 +1,25 @@ +import type { Query } from '@tanstack/svelte-query'; + +type UnknownQuery = Query; // failing a request n times const ERR_THRESHOLD = 1; class ConnectivityManager { apiError = $state(undefined); authError = $state(undefined); - reportError(error: Error) { - this.apiError = true; - } - reportSuccess(data: unknown) { - this.apiError = false; - } - reportAuthError(error: Error) { - this.authError = true; + reportError(error: Error, query: UnknownQuery) { + const tags = query.meta; + if (!(tags && tags.network)) return; //filter tagged reqs + // + if (tags.auth) this.authError = true; + else this.apiError = true; } - reportAuthSuccess(data: unknown) { - this.authError = false; + reportSuccess(data: unknown, query: UnknownQuery) { + const tags = query.meta; + if (!(tags && tags.network)) return; //filter tagged reqs + // + if (tags.auth) this.authError = false; + else this.apiError = false; } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 455a18b..5c7f498 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -12,23 +12,18 @@ import { onDestroy, onMount } from 'svelte'; import './layout.css'; import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; - import { Cloud, CloudOff } from '@lucide/svelte'; + import { CloudOff } from '@lucide/svelte'; import * as Card from '$lib/components/ui/card'; import { fly } from 'svelte/transition'; + import * as Empty from '$lib/components/ui/empty'; let { children } = $props(); const queryClient = new QueryClient({ defaultOptions: { queries: { networkMode: 'offlineFirst' } }, queryCache: new QueryCache({ - onError: (error, query) => { - if (query.meta?.network) connectivityManager.reportError(error); - if (query.meta?.auth) connectivityManager.reportAuthError(error); - }, - onSuccess: (data, query) => { - if (query.meta?.network) connectivityManager.reportSuccess(data); - if (query.meta?.auth) connectivityManager.reportAuthSuccess(data); - }, + onError: (error, query) => connectivityManager.reportError(error, query), + onSuccess: (data, query) => connectivityManager.reportSuccess(data, query), }), }); $effect(() => { @@ -60,14 +55,12 @@ opacity: 1, }} > - -
- -

Offline

-
- -
-
+ +
+ + + + Offline
From c33b8c3d77da693141359c0c462b05650c18f8f6 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sat, 15 Aug 2026 22:19:48 -0500 Subject: [PATCH 04/20] caching mechanism (still need to show) --- src/lib/data/app.ts | 1 + .../managers/connectivity.manager.svelte.ts | 46 +++++++++++++++++-- src/lib/managers/map.manager.svelte.ts | 1 - src/routes/+layout.svelte | 4 +- src/routes/+page.svelte | 15 +++--- 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/lib/data/app.ts b/src/lib/data/app.ts index 52e07a8..8f7f238 100644 --- a/src/lib/data/app.ts +++ b/src/lib/data/app.ts @@ -38,6 +38,7 @@ export const useRoutes = () => { const query = createDependencyQuery(() => ({ queryKey: [QueryKey.ROUTE_LIST], + meta: { isRoutes: true }, queryFn: async () => { queryLogger.i(`Loaded ${asRouteList.data?.length} routes from Aggie Spirit`); return asRouteList.data!; diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index cf23752..902dbb8 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -1,11 +1,20 @@ +import type { Route } from '$lib/data/types'; +import { Directory, Encoding, Filesystem, type WriteFileResult } from '@capacitor/filesystem'; import type { Query } from '@tanstack/svelte-query'; +import type { any, unknown } from 'zod'; type UnknownQuery = Query; -// failing a request n times -const ERR_THRESHOLD = 1; + +const ROUTES_FILE = 'cache/routes.json'; + class ConnectivityManager { - apiError = $state(undefined); - authError = $state(undefined); + cachedRoutes = $state(undefined); + apiError = $state(undefined); + authError = $state(undefined); + + constructor() { + this.tryUncache(); + } reportError(error: Error, query: UnknownQuery) { const tags = query.meta; @@ -16,11 +25,38 @@ class ConnectivityManager { } reportSuccess(data: unknown, query: UnknownQuery) { const tags = query.meta; - if (!(tags && tags.network)) return; //filter tagged reqs + if (!tags) return; + if (tags.isRoutes) this.tryCache(data as Route[]); + if (!tags.network) return; //filter tagged reqs // if (tags.auth) this.authError = false; else this.apiError = false; } + + private tryCache(data: Route[]): Promise | null { + if (this.cachedRoutes === data) return null; + this.cachedRoutes = data; + return Filesystem.writeFile({ + directory: Directory.Data, + path: ROUTES_FILE, + data: JSON.stringify(data), + encoding: Encoding.UTF8, + recursive: true, // make parents + }); + } + private async tryUncache() { + try { + const { data: raw } = await Filesystem.readFile({ + directory: Directory.Data, + path: ROUTES_FILE, + encoding: Encoding.UTF8, + }); + // no validation bc the cache will just overwrite + // 5 seconds after the app starts if the user messes + // w their own data + this.cachedRoutes = JSON.parse(raw as string) as Route[]; + } catch {} + } } // Persist connectivityManager across Svelte HMRs diff --git a/src/lib/managers/map.manager.svelte.ts b/src/lib/managers/map.manager.svelte.ts index 1a3543d..e3a2c90 100644 --- a/src/lib/managers/map.manager.svelte.ts +++ b/src/lib/managers/map.manager.svelte.ts @@ -37,7 +37,6 @@ class MapManager { setDrawnRoutes(routes: Route[], animateTo: boolean = true) { this.drawnRoutes = routes; - if (animateTo) { const allPoints = routes.flatMap((route) => route.directions.flatMap((direction) => diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5c7f498..3350178 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -26,9 +26,7 @@ onSuccess: (data, query) => connectivityManager.reportSuccess(data, query), }), }); - $effect(() => { - console.log(connectivityManager.apiError, connectivityManager.authError); - }); + onDestroy(() => { mapManager.unregisterMap(); }); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5da15f6..0c4b786 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,21 +8,22 @@ import Spinner from '$lib/components/ui/spinner/spinner.svelte'; import * as Tabs from '$lib/components/ui/tabs'; import { useRoutes } from '$lib/data/app'; + import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; import { frontPageManager } from '$lib/managers/frontpage.manager.svelte'; import { mapManager } from '$lib/managers/map.manager.svelte'; import { Preferences } from '@capacitor/preferences'; import { Cog, Route } from '@lucide/svelte'; import { onMount } from 'svelte'; - let routes = useRoutes(); + const routes = useRoutes(); + const routesData = $derived(routes.data ?? connectivityManager.cachedRoutes ?? []); + const favRoutes = $derived( - routes.data?.filter((route) => frontPageManager.favorites.includes(route.routeCode)) ?? [], + routesData.filter((route) => frontPageManager.favorites.includes(route.routeCode)), ); $effect(() => { - mapManager.setDrawnRoutes( - frontPageManager.selectedTab == 'all' ? (routes.data ?? []) : favRoutes, - ); + mapManager.setDrawnRoutes(frontPageManager.selectedTab == 'all' ? routesData : favRoutes); }); onMount(async () => frontPageManager.loadFavorites()); @@ -67,13 +68,13 @@ {#if frontPageManager.selectedTab === 'all'} - {#if routes.isLoading} + {#if routes.isLoading && !routesData.length} {:else if routes.isError}

Error loading routes: {routes.error.message}

{/if} - {#each routes.data ?? [] as route} + {#each routesData as route} goto(`/route/${route.id}`)} /> {/each}
From 7f34e7ad52a8f91ab6c7a4b83131ae80aecec443 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sat, 15 Aug 2026 22:53:01 -0500 Subject: [PATCH 05/20] proxy useRoutes() to attempt to pass cachedRoutes --- src/lib/data/app.ts | 10 +++++++++- src/lib/managers/connectivity.manager.svelte.ts | 3 ++- src/routes/+page.svelte | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/data/app.ts b/src/lib/data/app.ts index 8f7f238..6472f45 100644 --- a/src/lib/data/app.ts +++ b/src/lib/data/app.ts @@ -9,7 +9,9 @@ import { type StopSchedule, type TimeEstimate, } from '$lib/data/types'; +import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; import { queryLogger } from '$lib/utils/logger'; +import type { CreateQueryResult } from '@tanstack/svelte-query'; import moment from 'moment'; import { createDependencyQuery, createSelectableQuery } from '../utils/queries'; import { @@ -46,7 +48,13 @@ export const useRoutes = () => { dependents: [asRouteList], })); - return query; + // mask data to allow cached routes when present + return new Proxy(query, { + get: (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver); + return prop === 'data' ? (value ?? connectivityManager.cachedRoutes) : value; + }, + }) as CreateQueryResult; }; export const useVehicles = (params: () => { route: Route | null }) => { diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 902dbb8..348d0f0 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -5,10 +5,11 @@ import type { any, unknown } from 'zod'; type UnknownQuery = Query; +// TODO inline if this doesnt expand to anything else const ROUTES_FILE = 'cache/routes.json'; class ConnectivityManager { - cachedRoutes = $state(undefined); + cachedRoutes = $state([]); apiError = $state(undefined); authError = $state(undefined); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0c4b786..af7afaa 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,7 +8,6 @@ import Spinner from '$lib/components/ui/spinner/spinner.svelte'; import * as Tabs from '$lib/components/ui/tabs'; import { useRoutes } from '$lib/data/app'; - import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; import { frontPageManager } from '$lib/managers/frontpage.manager.svelte'; import { mapManager } from '$lib/managers/map.manager.svelte'; import { Preferences } from '@capacitor/preferences'; @@ -16,7 +15,7 @@ import { onMount } from 'svelte'; const routes = useRoutes(); - const routesData = $derived(routes.data ?? connectivityManager.cachedRoutes ?? []); + const routesData = $derived(routes.data ?? []); const favRoutes = $derived( routesData.filter((route) => frontPageManager.favorites.includes(route.routeCode)), From dd75cdedea2355338629ac1bcbaacbeec72ddaa4 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sat, 15 Aug 2026 23:57:43 -0500 Subject: [PATCH 06/20] 5 hours of coding will save you from 5 minutes of documentation or something --- src/lib/data/app.ts | 9 ++------- src/lib/utils/queries.ts | 2 ++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/data/app.ts b/src/lib/data/app.ts index 6472f45..99eaaf9 100644 --- a/src/lib/data/app.ts +++ b/src/lib/data/app.ts @@ -41,6 +41,7 @@ export const useRoutes = () => { const query = createDependencyQuery(() => ({ queryKey: [QueryKey.ROUTE_LIST], meta: { isRoutes: true }, + placeholderData: connectivityManager.cachedRoutes, queryFn: async () => { queryLogger.i(`Loaded ${asRouteList.data?.length} routes from Aggie Spirit`); return asRouteList.data!; @@ -48,13 +49,7 @@ export const useRoutes = () => { dependents: [asRouteList], })); - // mask data to allow cached routes when present - return new Proxy(query, { - get: (target, prop, receiver) => { - const value = Reflect.get(target, prop, receiver); - return prop === 'data' ? (value ?? connectivityManager.cachedRoutes) : value; - }, - }) as CreateQueryResult; + return query; }; export const useVehicles = (params: () => { route: Route | null }) => { diff --git a/src/lib/utils/queries.ts b/src/lib/utils/queries.ts index 169c87d..996e6be 100644 --- a/src/lib/utils/queries.ts +++ b/src/lib/utils/queries.ts @@ -6,6 +6,7 @@ interface Params { queryFn: () => Promise; queryKey: any[]; staleTime?: moment.Duration | number; + placeholderData?: any; refetchInterval?: moment.Duration | number; enabled?: boolean; meta?: Record; @@ -31,6 +32,7 @@ export function createDependencyQuery(params: () => DependencyQueryParams) return { queryKey: [...p.queryKey, ...p.dependents.map((q) => q.data)], meta: p.meta, + placeholderData: p.placeholderData, queryFn: async () => { try { const start = moment.now(); From 5e302e1e831ba48c53b8b011a6833df2f4292123 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 15:10:21 -0500 Subject: [PATCH 07/20] requery loops w backoff and concrete status resolution --- .../managers/connectivity.manager.svelte.ts | 98 ++++++++++++++++++- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 348d0f0..1b7a8a3 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -1,28 +1,96 @@ import type { Route } from '$lib/data/types'; import { Directory, Encoding, Filesystem, type WriteFileResult } from '@capacitor/filesystem'; -import type { Query } from '@tanstack/svelte-query'; -import type { any, unknown } from 'zod'; +import type { Query, QueryClient } from '@tanstack/svelte-query'; +import type { Connect } from 'vite'; type UnknownQuery = Query; // TODO inline if this doesnt expand to anything else const ROUTES_FILE = 'cache/routes.json'; +const RETRY_MIN = 5e3; +const RETRY_MAX = 300e3; + +export enum ConnectionStatus { + OFFLINE, + CONNECTING, + RECONNECTING, + ONLINE, +} +export namespace ConnectionStatus { + export function asMessage(s: ConnectionStatus): string { + switch (s) { + case ConnectionStatus.OFFLINE: + return 'Offline'; + case ConnectionStatus.CONNECTING: + return 'Connecting...'; + case ConnectionStatus.RECONNECTING: + return 'Reconnecting...'; + case ConnectionStatus.ONLINE: + return 'Online'; + default: + return ''; + } + } +} class ConnectivityManager { + private client = $state(undefined); + + private retryAfter = RETRY_MIN; + private mayRetryAt = $state(0); + private isRetrying = $state(false); + cachedRoutes = $state([]); - apiError = $state(undefined); - authError = $state(undefined); + apiError = $state(undefined); // tamu error + authError = $state(undefined); // auth.maroonrides.app + isError = $derived(this.apiError || this.authError); + + //whether or not to pick CONNECTING/RECONNECTING + private hasFullyConnectedBefore = $state(false); + private isFullyConnected = $derived(this.apiError === false && this.authError === false); constructor() { this.tryUncache(); } + shareClient(q: QueryClient) { + this.client = q; + } + private backoff() { + this.retryAfter = Math.min(RETRY_MAX, this.retryAfter * 2); + } + private resetBackoff() { + this.retryAfter = RETRY_MIN; + } + + // if you ever use parallel js (hahaha) use CAS + private tryEnqueueReconnect(failureTime: number) { + if (!this.client || failureTime <= this.mayRetryAt) return; + if (this.isRetrying) return; //arbitrary time for refetch to finish + // + const delay = this.retryAfter; + this.mayRetryAt = failureTime + delay; + this.backoff(); + // + setTimeout(async () => { + if (!this.client) return; + this.isRetrying = true; + try { + await this.client.refetchQueries({ predicate: (q) => q.state.status === 'error' }); + } finally { + this.isRetrying = false; + if (this.isError) this.tryEnqueueReconnect(Date.now()); + } + }, delay); + } reportError(error: Error, query: UnknownQuery) { const tags = query.meta; - if (!(tags && tags.network)) return; //filter tagged reqs + if (this.isRetrying || !(tags && tags.network)) return; //filter tagged reqs // if (tags.auth) this.authError = true; else this.apiError = true; + // + this.tryEnqueueReconnect(Date.now()); } reportSuccess(data: unknown, query: UnknownQuery) { const tags = query.meta; @@ -32,8 +100,28 @@ class ConnectivityManager { // if (tags.auth) this.authError = false; else this.apiError = false; + // + if (!this.isFullyConnected) return; + this.resetBackoff(); + this.hasFullyConnectedBefore = true; } + //arrow function so svelte can use reactively + getConnectionStatus = () => { + if (this.isError && this.isRetrying) { + return this.hasFullyConnectedBefore + ? ConnectionStatus.RECONNECTING + : ConnectionStatus.CONNECTING; + } + // only give connecting message when cache is loaded + if (this.authError === undefined) return ConnectionStatus.CONNECTING; + if (this.authError === true) return ConnectionStatus.OFFLINE; + + if (this.apiError === undefined) return ConnectionStatus.CONNECTING; + if (this.apiError === true) return ConnectionStatus.OFFLINE; + return ConnectionStatus.ONLINE; + }; + private tryCache(data: Route[]): Promise | null { if (this.cachedRoutes === data) return null; this.cachedRoutes = data; From 0bac4104edc650604fbce356037478aeb7003e24 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 15:10:59 -0500 Subject: [PATCH 08/20] move to ConnectionBar component + relevant changes --- src/lib/components/ConnectionPill.svelte | 47 ++++++++++++++++++++++++ src/routes/+layout.svelte | 26 ++----------- src/routes/+page.svelte | 4 +- 3 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 src/lib/components/ConnectionPill.svelte diff --git a/src/lib/components/ConnectionPill.svelte b/src/lib/components/ConnectionPill.svelte new file mode 100644 index 0000000..ad4f11f --- /dev/null +++ b/src/lib/components/ConnectionPill.svelte @@ -0,0 +1,47 @@ + + +{#if mayShow && isBadStatus} + {@const Icon = status === ConnectionStatus.OFFLINE ? CloudOff : Spinner} +
+ +
+ + + + + {ConnectionStatus.asMessage(status)} + +
+
+
+{/if} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3350178..ab86147 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -12,10 +12,7 @@ import { onDestroy, onMount } from 'svelte'; import './layout.css'; import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; - import { CloudOff } from '@lucide/svelte'; - import * as Card from '$lib/components/ui/card'; - import { fly } from 'svelte/transition'; - import * as Empty from '$lib/components/ui/empty'; + import ConnectionPill from '$lib/components/ConnectionPill.svelte'; let { children } = $props(); @@ -32,6 +29,7 @@ }); onMount(async () => { + connectivityManager.shareClient(queryClient); migratePrefs(); }); @@ -44,25 +42,7 @@ - {#if connectivityManager.apiError || connectivityManager.authError} -
- -
- - - - Offline -
-
-
- {/if} +
{#if routes.isLoading && !routesData.length} - {:else if routes.isError} -

Error loading routes: {routes.error.message}

+ {/if} {#each routesData as route} From 7ed5c6b008976ad741edf2b7e139f6901fef9d01 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 15:33:47 -0500 Subject: [PATCH 09/20] make pill; claude did the anims --- src/lib/components/ConnectionPill.svelte | 111 +++++++++++++++++++---- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/src/lib/components/ConnectionPill.svelte b/src/lib/components/ConnectionPill.svelte index ad4f11f..b6b085f 100644 --- a/src/lib/components/ConnectionPill.svelte +++ b/src/lib/components/ConnectionPill.svelte @@ -1,7 +1,7 @@ -{#if mayShow && isBadStatus} - {@const Icon = status === ConnectionStatus.OFFLINE ? CloudOff : Spinner} +{#if mayShow && isBadStatus && shownStatus !== undefined}
- -
- - - - - {ConnectionStatus.asMessage(status)} - -
-
+
+ + +
+ + + + + {ConnectionStatus.asMessage(shownStatus)} + +
+
+
{/if} From a3671d462bd69e207c4747308064c3d5cbaf574d Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 15:35:19 -0500 Subject: [PATCH 10/20] simpler anim alternative for prior commit --- src/lib/components/ConnectionPill.svelte | 78 +++--------------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/src/lib/components/ConnectionPill.svelte b/src/lib/components/ConnectionPill.svelte index b6b085f..589b422 100644 --- a/src/lib/components/ConnectionPill.svelte +++ b/src/lib/components/ConnectionPill.svelte @@ -1,7 +1,7 @@ -{#if mayShow && isBadStatus && shownStatus !== undefined} +{#if mayShow && isBadStatus} + {@const Icon = status === ConnectionStatus.OFFLINE ? CloudOff : Spinner}
-
- - {ConnectionStatus.asMessage(shownStatus)} + + {ConnectionStatus.asMessage(status)}
From 75ccc57a7c56e23ea7ea2d66982849e3fe53f0ce Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 18:53:51 -0500 Subject: [PATCH 11/20] dev button for invalidating cache (regen uuids) --- android/app/capacitor.build.gradle | 2 ++ android/capacitor.settings.gradle | 6 ++++ ios/App/CapApp-SPM/Package.swift | 4 +++ package.json | 2 ++ pnpm-lock.yaml | 24 +++++++++++++ src/lib/components/ui/map/MapControls.svelte | 35 ++++++++++++++++++- .../managers/connectivity.manager.svelte.ts | 18 ++++++++++ 7 files changed, 90 insertions(+), 1 deletion(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 95bad48..c1af675 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,6 +9,8 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-app') + implementation project(':capacitor-dialog') implementation project(':capacitor-filesystem') implementation project(':capacitor-geolocation') implementation project(':capacitor-preferences') diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 8f2abec..7c2c6fd 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,12 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.0_patch_hash=5f151b861cf2cde88167eece495aecb0862a61bea5115b27870_874123a7c3556940d002a867e09a2b84/node_modules/@capacitor/android/capacitor') +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.1_@capacitor+core@8.3.0/node_modules/@capacitor/app/android') + +include ':capacitor-dialog' +project(':capacitor-dialog').projectDir = new File('../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/dialog/android') + include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.0/node_modules/@capacitor/filesystem/android') diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index 47eafb8..9579212 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -12,6 +12,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.0"), + .package(name: "CapacitorApp", path: "../../../node_modules/.pnpm/@capacitor+app@8.1.1_@capacitor+core@8.3.0/node_modules/@capacitor/app"), + .package(name: "CapacitorDialog", path: "../../../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/dialog"), .package(name: "CapacitorFilesystem", path: "../../../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.0/node_modules/@capacitor/filesystem"), .package(name: "CapacitorGeolocation", path: "../../../node_modules/.pnpm/@capacitor+geolocation@8.0.0_@capacitor+core@8.3.0/node_modules/@capacitor/geolocation"), .package(name: "CapacitorPreferences", path: "../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.0/node_modules/@capacitor/preferences") @@ -22,6 +24,8 @@ let package = Package( dependencies: [ .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), + .product(name: "CapacitorApp", package: "CapacitorApp"), + .product(name: "CapacitorDialog", package: "CapacitorDialog"), .product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"), .product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"), .product(name: "CapacitorPreferences", package: "CapacitorPreferences") diff --git a/package.json b/package.json index 4f3168c..659453f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ }, "dependencies": { "@capacitor/android": "8.3.0", + "@capacitor/app": "^8.1.1", "@capacitor/core": "^8.3.0", + "@capacitor/dialog": "^8.0.1", "@capacitor/filesystem": "^8.1.2", "@capacitor/geolocation": "^8.0.0", "@capacitor/ios": "^8.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe974d5..125f627 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,15 @@ importers: '@capacitor/android': specifier: 8.3.0 version: 8.3.0(patch_hash=5f151b861cf2cde88167eece495aecb0862a61bea5115b27870240ddf729efdb)(@capacitor/core@8.3.0) + '@capacitor/app': + specifier: ^8.1.1 + version: 8.1.1(@capacitor/core@8.3.0) '@capacitor/core': specifier: ^8.3.0 version: 8.3.0 + '@capacitor/dialog': + specifier: ^8.0.1 + version: 8.0.1(@capacitor/core@8.3.0) '@capacitor/filesystem': specifier: ^8.1.2 version: 8.1.2(@capacitor/core@8.3.0) @@ -131,6 +137,11 @@ packages: peerDependencies: '@capacitor/core': ^8.3.0 + '@capacitor/app@8.1.1': + resolution: {integrity: sha512-xM2ZTX5jK60tFtmjsmJv+Cdj1Qsb6NcavkqmdEnCPQBeya9oKhamZJxYOnQNj1ZvcJM7sWfG6BEtSLx42pisbA==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + '@capacitor/cli@8.0.2': resolution: {integrity: sha512-/8qLYxhytMyUKTHK8i6YU+DMD3AuFiQgSuJCyMltcg9MN3W9En7zqQZSo/WN4eC7qif/oyZACzm7OkAZKani7g==} engines: {node: '>=22.0.0'} @@ -139,6 +150,11 @@ packages: '@capacitor/core@8.3.0': resolution: {integrity: sha512-S4ajn4G/fS3VJj8salxqH/3LO5PPWv1VxGKQ27OCajnDcLJjEg9VXwgMPnlypgkIOqCJ2fmQLtk8GT+BlI9/rw==} + '@capacitor/dialog@8.0.1': + resolution: {integrity: sha512-Dq0mZBsd++7CvPvpD2gfbUB44k/zCoe5K9IgYmMImQ4Yphu4/MxLVpFDFAqSya011UBBTGPrUQGVacIW3jDNkw==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + '@capacitor/filesystem@8.1.2': resolution: {integrity: sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==} peerDependencies: @@ -1550,6 +1566,10 @@ snapshots: dependencies: '@capacitor/core': 8.3.0 + '@capacitor/app@8.1.1(@capacitor/core@8.3.0)': + dependencies: + '@capacitor/core': 8.3.0 + '@capacitor/cli@8.0.2': dependencies: '@ionic/cli-framework-output': 2.2.8 @@ -1576,6 +1596,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@capacitor/dialog@8.0.1(@capacitor/core@8.3.0)': + dependencies: + '@capacitor/core': 8.3.0 + '@capacitor/filesystem@8.1.2(@capacitor/core@8.3.0)': dependencies: '@capacitor/core': 8.3.0 diff --git a/src/lib/components/ui/map/MapControls.svelte b/src/lib/components/ui/map/MapControls.svelte index d6bb21e..b8cd6c3 100644 --- a/src/lib/components/ui/map/MapControls.svelte +++ b/src/lib/components/ui/map/MapControls.svelte @@ -2,11 +2,15 @@ import { mapManager } from '$lib/managers/map.manager.svelte'; import { cn } from '$lib/utils.js'; import { Geolocation } from '@capacitor/geolocation'; - import { Locate, LocateFixed } from '@lucide/svelte'; + import { Braces, Locate, LocateFixed } from '@lucide/svelte'; import { getContext } from 'svelte'; import Button from '../button/button.svelte'; import Spinner from '../spinner/spinner.svelte'; import type { MapContext } from './Map.svelte'; + import { connectivityManager } from '$lib/managers/connectivity.manager.svelte'; + import { Dialog } from '@capacitor/dialog'; + import { Capacitor, CapacitorException } from '@capacitor/core'; + import { App } from '@capacitor/app'; interface Props { position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; showZoom?: boolean; @@ -67,6 +71,25 @@ waitingForLocation = false; } } + async function handleCacheScramble() { + const { message, status } = await connectivityManager.scrambleCacheUUIDS(); + await Dialog.alert({ + title: 'Cache Scrambler', + message: message, + }); + if (!status) return; + // true = exit + try { + await App.exitApp(); + } catch (error) { + if (!(error instanceof CapacitorException)) return; + await Dialog.alert({ + title: 'Cache Scrambler', + message: 'assuming webpage... reloading', + }); + window.location.reload(); + } + } {#if loaded} @@ -86,5 +109,15 @@ {/if} + +
{/if} diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 1b7a8a3..dba9790 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -2,6 +2,7 @@ import type { Route } from '$lib/data/types'; import { Directory, Encoding, Filesystem, type WriteFileResult } from '@capacitor/filesystem'; import type { Query, QueryClient } from '@tanstack/svelte-query'; import type { Connect } from 'vite'; +import { uuid } from 'zod'; type UnknownQuery = Query; @@ -53,6 +54,23 @@ class ConnectivityManager { this.tryUncache(); } + // for testing dont expose to user durrr + async scrambleCacheUUIDS(): Promise<{ status: boolean; message: string }> { + const numRoutes = this.cachedRoutes.length; + if (!numRoutes) return { message: 'There is no cached copy to modify yet.', status: false }; + const mod = this.cachedRoutes.map((r) => { + return { + ...r, + id: crypto.randomUUID(), + }; + }); + const result = await this.tryCache(mod); + if (!result) return { message: 'Failed Route[] comparison', status: false }; + return { + message: `Regenerated ${numRoutes} cached UUIDS. The app will now exit.`, + status: true, + }; + } shareClient(q: QueryClient) { this.client = q; } From b774c89a3fef842696b457ea57fc62ade822b25b Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 19:06:11 -0500 Subject: [PATCH 12/20] (more) explicit debugging mode for the con man --- src/lib/components/ui/map/MapControls.svelte | 20 ++++++++++--------- .../managers/connectivity.manager.svelte.ts | 4 ++++ src/routes/+layout.svelte | 2 ++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib/components/ui/map/MapControls.svelte b/src/lib/components/ui/map/MapControls.svelte index b8cd6c3..9b06316 100644 --- a/src/lib/components/ui/map/MapControls.svelte +++ b/src/lib/components/ui/map/MapControls.svelte @@ -110,14 +110,16 @@ {/if} - + {#if connectivityManager.isDevMode} + + {/if}
{/if} diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index dba9790..597085b 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -35,6 +35,9 @@ export namespace ConnectionStatus { } class ConnectivityManager { + // for debug + isDevMode = $state(false); + private client = $state(undefined); private retryAfter = RETRY_MIN; @@ -56,6 +59,7 @@ class ConnectivityManager { // for testing dont expose to user durrr async scrambleCacheUUIDS(): Promise<{ status: boolean; message: string }> { + if (!this.isDevMode) return { message: 'Dev mode is not enabled.', status: false }; const numRoutes = this.cachedRoutes.length; if (!numRoutes) return { message: 'There is no cached copy to modify yet.', status: false }; const mod = this.cachedRoutes.map((r) => { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ab86147..31f5854 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -30,6 +30,8 @@ onMount(async () => { connectivityManager.shareClient(queryClient); + connectivityManager.isDevMode = true; //enables scrambling button + migratePrefs(); }); From 1926396fe6e8ab2e119e778963d9688abbe5ab95 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 20:50:12 -0500 Subject: [PATCH 13/20] cache checking funcs --- .../managers/connectivity.manager.svelte.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 597085b..4727e54 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -45,6 +45,9 @@ class ConnectivityManager { private isRetrying = $state(false); cachedRoutes = $state([]); + cacheWasStale = $state(undefined); //flicks true when ids change, requires connection obviously + cacheIsSynced = $state(false); + apiError = $state(undefined); // tamu error authError = $state(undefined); // auth.maroonrides.app isError = $derived(this.apiError || this.authError); @@ -68,7 +71,7 @@ class ConnectivityManager { id: crypto.randomUUID(), }; }); - const result = await this.tryCache(mod); + const result = await this.tryCache(mod, true); if (!result) return { message: 'Failed Route[] comparison', status: false }; return { message: `Regenerated ${numRoutes} cached UUIDS. The app will now exit.`, @@ -143,10 +146,24 @@ class ConnectivityManager { if (this.apiError === true) return ConnectionStatus.OFFLINE; return ConnectionStatus.ONLINE; }; + private checkForUUIDDesync(cache: Route[], cmp: Route[]): boolean { + if (cache.length !== cmp.length) return true; - private tryCache(data: Route[]): Promise | null { + const ids = new Map(cache.map((r) => [r.routeCode, r.id])); + return cmp.some((r) => ids.has(r.routeCode) && ids.get(r.routeCode) !== r.id); + } + private tryCache( + data: Route[], + noUpdateMemory: boolean = false, + ): Promise | null { if (this.cachedRoutes === data) return null; - this.cachedRoutes = data; + if (!noUpdateMemory) { + const wasStale = this.checkForUUIDDesync(this.cachedRoutes, data); + console.log(wasStale); + this.cachedRoutes = data; + this.cacheWasStale = wasStale; + this.cacheIsSynced = !wasStale; + } return Filesystem.writeFile({ directory: Directory.Data, path: ROUTES_FILE, From b3f3a19cad2efae1ea73eb6e5c2668b24709f1c6 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 21:40:46 -0500 Subject: [PATCH 14/20] add placeholderData field to both features (for consistency) --- src/lib/utils/queries.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/utils/queries.ts b/src/lib/utils/queries.ts index 996e6be..26767d3 100644 --- a/src/lib/utils/queries.ts +++ b/src/lib/utils/queries.ts @@ -129,6 +129,7 @@ interface SelectableQueryParams { staleTime?: moment.Duration | number; refetchInterval?: moment.Duration | number; enabled?: boolean; + placeholderData?: any; queries: Partial>>; unsupportedValue?: T; selector?: S; @@ -156,6 +157,7 @@ export function createSelectableQuery( throw e; } }, + placeholderData: p.placeholderData, enabled: (p.enabled ?? true) && (selectedQuery?.isSuccess ?? false), staleTime: parseTime(p.staleTime), refetchInterval: parseTime(p.refetchInterval), From b89956ca24a1c233115bb4809ff4c7e0109cc824 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 21:42:46 -0500 Subject: [PATCH 15/20] an example proposal basing pages off [id] (which is pretty bad) --- .../managers/connectivity.manager.svelte.ts | 3 +- src/routes/route/[id]/+page.svelte | 20 ++++++++- src/routes/route/[id]/OfflineStopRow.svelte | 43 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/routes/route/[id]/OfflineStopRow.svelte diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 4727e54..101547f 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -159,10 +159,9 @@ class ConnectivityManager { if (this.cachedRoutes === data) return null; if (!noUpdateMemory) { const wasStale = this.checkForUUIDDesync(this.cachedRoutes, data); - console.log(wasStale); this.cachedRoutes = data; this.cacheWasStale = wasStale; - this.cacheIsSynced = !wasStale; + this.cacheIsSynced = true; } return Filesystem.writeFile({ directory: Directory.Data, diff --git a/src/routes/route/[id]/+page.svelte b/src/routes/route/[id]/+page.svelte index 9f21025..aaa61df 100644 --- a/src/routes/route/[id]/+page.svelte +++ b/src/routes/route/[id]/+page.svelte @@ -1,7 +1,7 @@ + +
+
+
+ +

{subtitle}

+
+
+
+ +
+ +
+
From a8424138f75964ca6c2543f74d2a146c02404529 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 22:39:51 -0500 Subject: [PATCH 16/20] initial move to routeCode --- src/lib/components/map/MapElements.svelte | 12 ++++++------ .../managers/connectivity.manager.svelte.ts | 6 +++++- src/routes/+page.svelte | 4 ++-- .../route/{[id] => [routeCode]}/+page.svelte | 18 +++--------------- .../{[id]/alerts => [routeCode]}/+page.ts | 2 +- .../{[id] => [routeCode]}/EstimateRow.svelte | 0 .../OfflineStopRow.svelte | 14 ++++++++++++-- .../route/{[id] => [routeCode]}/StopRow.svelte | 2 +- .../{[id] => [routeCode]}/alerts/+page.svelte | 4 ++-- .../{[id] => [routeCode]/alerts}/+page.ts | 2 +- .../timetable/[stop]/[direction]/+page.svelte | 8 ++++---- .../timetable/[stop]/[direction]/+page.ts | 2 +- 12 files changed, 38 insertions(+), 36 deletions(-) rename src/routes/route/{[id] => [routeCode]}/+page.svelte (87%) rename src/routes/route/{[id]/alerts => [routeCode]}/+page.ts (77%) rename src/routes/route/{[id] => [routeCode]}/EstimateRow.svelte (100%) rename src/routes/route/{[id] => [routeCode]}/OfflineStopRow.svelte (64%) rename src/routes/route/{[id] => [routeCode]}/StopRow.svelte (97%) rename src/routes/route/{[id] => [routeCode]}/alerts/+page.svelte (93%) rename src/routes/route/{[id] => [routeCode]/alerts}/+page.ts (77%) rename src/routes/route/{[id] => [routeCode]}/timetable/[stop]/[direction]/+page.svelte (88%) rename src/routes/route/{[id] => [routeCode]}/timetable/[stop]/[direction]/+page.ts (84%) diff --git a/src/lib/components/map/MapElements.svelte b/src/lib/components/map/MapElements.svelte index 26a7566..50bb4a0 100644 --- a/src/lib/components/map/MapElements.svelte +++ b/src/lib/components/map/MapElements.svelte @@ -16,7 +16,7 @@ import { goto } from '$app/navigation'; const routes = useRoutes(); - const drawnRouteIds = $derived(mapManager.drawnRoutes.map((route) => route.id)); + const drawnRouteIds = $derived(mapManager.drawnRoutes.map((route) => route.routeCode)); const busLocations = $derived(useVehicles(() => ({ route: mapManager.selectedRoute }))); let userLocation: Location | null = $state(null); @@ -58,21 +58,21 @@ flushScheduled = false; const topmost = requestedRoutes.reduce((a, b) => (b.index > a.index ? b : a)).route; requestedRoutes.length = 0; //cursed - goto(`/route/${topmost.id}`); + goto(`/route/${topmost.routeCode}`); } {#snippet routeLine(route: Route, index: number)} - {#each route.directions as direction (`${route.id}-${direction.id}`)} + {#each route.directions as direction (`${route.routeCode}-${direction.id}`)} {@const isSelected = direction.id === mapManager.selectedDirectionId || mapManager.selectedDirectionId == ''} - {@const isShown = drawnRouteIds.includes(route.id)} + {@const isShown = drawnRouteIds.includes(route.routeCode)} {@const opacity = isShown ? (isSelected ? 1 : 0.5) : 0} [point.longitude, point.latitude])} color={getRouteTint(route, themeManager.theme)} - id={`${route.id}-${direction.id}`} + id={`${route.routeCode}-${direction.id}`} onclick={() => { if (!isSelected) return; //opacity check instead of isShown for ghost directional routes requestRoute(route, index); @@ -103,7 +103,7 @@ {/snippet} -{#each routes?.data as route, i (route.id)} +{#each routes?.data as route, i (route.routeCode)} {@render routeLine(route, i)} {/each} diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index 101547f..e9a7554 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -46,7 +46,7 @@ class ConnectivityManager { cachedRoutes = $state([]); cacheWasStale = $state(undefined); //flicks true when ids change, requires connection obviously - cacheIsSynced = $state(false); + cacheIsSynced = $state(false); apiError = $state(undefined); // tamu error authError = $state(undefined); // auth.maroonrides.app @@ -146,6 +146,10 @@ class ConnectivityManager { if (this.apiError === true) return ConnectionStatus.OFFLINE; return ConnectionStatus.ONLINE; }; + // only true when cache is synced and route id exists + validate(route: Route): boolean { + return this.cacheIsSynced && this.cachedRoutes.some((r) => r.id === route.id); + } private checkForUUIDDesync(cache: Route[], cmp: Route[]): boolean { if (cache.length !== cmp.length) return true; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index dd154f8..5172b05 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -74,7 +74,7 @@ {/if} {#each routesData as route} - goto(`/route/${route.id}`)} /> + goto(`/route/${route.routeCode}`)} /> {/each} {:else if frontPageManager.selectedTab === 'favorites'} @@ -88,7 +88,7 @@ {/if} {#each favRoutes as route} - goto(`/route/${route.id}`)} /> + goto(`/route/${route.routeCode}`)} /> {/each} {/if} diff --git a/src/routes/route/[id]/+page.svelte b/src/routes/route/[routeCode]/+page.svelte similarity index 87% rename from src/routes/route/[id]/+page.svelte rename to src/routes/route/[routeCode]/+page.svelte index aaa61df..7312753 100644 --- a/src/routes/route/[id]/+page.svelte +++ b/src/routes/route/[routeCode]/+page.svelte @@ -21,7 +21,7 @@ let { data }: { data: PageData } = $props(); const routes = useRoutes(); - const route = $derived(routes.data?.find((r) => r.id === data.routeId) ?? null); + const route = $derived(routes.data?.find((r) => r.routeCode === data.routeCode) ?? null); const alerts = useAlerts(() => ({ route })); let isFavorite = $state(false); @@ -29,18 +29,6 @@ route?.directions.find((d) => d.id === mapManager.selectedDirectionId) ?? null, ); - let routeCode: string; - $effect(() => { - if (route !== null) routeCode = route.routeCode; - if (connectivityManager.cacheIsSynced && route === null) { - const movedRoute = routes.data?.find((r) => r.routeCode === routeCode) ?? null; - if (!movedRoute) goto('/'); - else - goto(`/route/${movedRoute.id}`, { - replaceState: true, - }); - } - }); onMount(async () => { const favoritedRoutes = JSON.parse( (await Preferences.get({ key: 'favorites' })).value ?? '[]', @@ -92,7 +80,7 @@ variant="outline" size="md" class="rounded-full" - onclick={() => goto(`/route/${route?.id}/alerts`)} + onclick={() => goto(`/route/${route?.routeCode}/alerts`)} > {#if alerts.data?.length ?? 0 > 0} @@ -117,7 +105,7 @@ {#if selectedDirection && route}
{#each selectedDirection?.stops ?? [] as stop, i (`${stop.id}-${selectedDirection?.id}-${i}`)} - {@const Row = connectivityManager.cacheIsSynced ? StopRow : OfflineStopRow} + {@const Row = connectivityManager.validate(route) ? StopRow : OfflineStopRow} {@const isLast = i === (selectedDirection?.stops.length ?? 0) - 1} {@const altDirection = isLast ? route.directions.length > 1 diff --git a/src/routes/route/[id]/alerts/+page.ts b/src/routes/route/[routeCode]/+page.ts similarity index 77% rename from src/routes/route/[id]/alerts/+page.ts rename to src/routes/route/[routeCode]/+page.ts index 4c13d23..e3b6c90 100644 --- a/src/routes/route/[id]/alerts/+page.ts +++ b/src/routes/route/[routeCode]/+page.ts @@ -2,6 +2,6 @@ import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params }) => { return { - routeId: params.id, + routeCode: params.routeCode, }; }; diff --git a/src/routes/route/[id]/EstimateRow.svelte b/src/routes/route/[routeCode]/EstimateRow.svelte similarity index 100% rename from src/routes/route/[id]/EstimateRow.svelte rename to src/routes/route/[routeCode]/EstimateRow.svelte diff --git a/src/routes/route/[id]/OfflineStopRow.svelte b/src/routes/route/[routeCode]/OfflineStopRow.svelte similarity index 64% rename from src/routes/route/[id]/OfflineStopRow.svelte rename to src/routes/route/[routeCode]/OfflineStopRow.svelte index f560683..ae4c745 100644 --- a/src/routes/route/[id]/OfflineStopRow.svelte +++ b/src/routes/route/[routeCode]/OfflineStopRow.svelte @@ -7,6 +7,8 @@ import { useStopAmenities, useStopEstimate } from '$lib/data/app'; import { Amenity, type Direction, type Route, type Stop } from '$lib/data/types'; import { CalendarIcon } from '@lucide/svelte'; + import { ConnectionStatus, connectivityManager } from '$lib/managers/connectivity.manager.svelte'; + import { Spinner } from '$lib/components/ui/spinner'; type Props = { stop: Stop; @@ -15,9 +17,14 @@ estimateDirection?: Direction; estimateStop?: Stop; }; + const conStatus = $derived.by(connectivityManager.getConnectionStatus); + const isLoading = $derived( + conStatus === ConnectionStatus.CONNECTING || conStatus === ConnectionStatus.RECONNECTING, + ); + const isOffline = $derived(conStatus === ConnectionStatus.OFFLINE); let { stop, route, direction, estimateDirection, estimateStop }: Props = $props(); - let subtitle = 'Depatures unavailable offline'; + let subtitle = $derived(isOffline ? 'Depatures unavailable offline' : 'No upcoming departures');
@@ -30,12 +37,15 @@ > {stop.name} +

{subtitle}

-
+
+ {#if isLoading}{/if} +
diff --git a/src/routes/route/[id]/StopRow.svelte b/src/routes/route/[routeCode]/StopRow.svelte similarity index 97% rename from src/routes/route/[id]/StopRow.svelte rename to src/routes/route/[routeCode]/StopRow.svelte index 8e86294..8d7a5ad 100644 --- a/src/routes/route/[id]/StopRow.svelte +++ b/src/routes/route/[routeCode]/StopRow.svelte @@ -84,7 +84,7 @@ size="sm" class="rounded-full" onclick={() => { - goto(`/route/${route?.id}/timetable/${stop.id}/${direction?.id}`); + goto(`/route/${route?.routeCode}/timetable/${stop.id}/${direction?.id}`); }} > diff --git a/src/routes/route/[id]/alerts/+page.svelte b/src/routes/route/[routeCode]/alerts/+page.svelte similarity index 93% rename from src/routes/route/[id]/alerts/+page.svelte rename to src/routes/route/[routeCode]/alerts/+page.svelte index 694960e..bbd2c10 100644 --- a/src/routes/route/[id]/alerts/+page.svelte +++ b/src/routes/route/[routeCode]/alerts/+page.svelte @@ -12,11 +12,11 @@ let { data }: { data: PageData } = $props(); const routes = useRoutes(); - const route = $derived(routes.data?.find((r) => r.id === data.routeId) ?? null); + const route = $derived(routes.data?.find((r) => r.routeCode === data.routeCode) ?? null); const alerts = useAlerts(() => ({ route })); function onClose() { - goto(`/route/${route?.id}`); + goto(`/route/${route?.routeCode}`); } const descriptionClasses = diff --git a/src/routes/route/[id]/+page.ts b/src/routes/route/[routeCode]/alerts/+page.ts similarity index 77% rename from src/routes/route/[id]/+page.ts rename to src/routes/route/[routeCode]/alerts/+page.ts index 4c13d23..e3b6c90 100644 --- a/src/routes/route/[id]/+page.ts +++ b/src/routes/route/[routeCode]/alerts/+page.ts @@ -2,6 +2,6 @@ import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params }) => { return { - routeId: params.id, + routeCode: params.routeCode, }; }; diff --git a/src/routes/route/[id]/timetable/[stop]/[direction]/+page.svelte b/src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.svelte similarity index 88% rename from src/routes/route/[id]/timetable/[stop]/[direction]/+page.svelte rename to src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.svelte index 25580f4..a6a2d4f 100644 --- a/src/routes/route/[id]/timetable/[stop]/[direction]/+page.svelte +++ b/src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.svelte @@ -21,14 +21,14 @@ const routes = useRoutes(); - const route = $derived(routes.data?.find((r) => r.id === data.routeId) ?? null); + const route = $derived(routes.data?.find((r) => r.routeCode === data.routeCode) ?? null); const direction = $derived(route?.directions.find((d) => d.id === data.directionId) ?? null); const stop = $derived(direction?.stops.find((s) => s.id === data.stopId) ?? null); const timetable = useStopSchedule(() => ({ stop, date })); function onClose() { - goto(`/route/${route?.id}`); + goto(`/route/${route?.routeCode}`); } @@ -56,12 +56,12 @@ {/if} {#each timetable.data as table} - {#if table.route.id == route?.id && table.direction.id == direction?.id} + {#if table.route.routeCode == route?.routeCode && table.direction.id == direction?.id}
goto(`/route/${table.route.id}`)} + onclick={() => goto(`/route/${table.route.routeCode}`)} /> {#if table.timetable.length === 0}

No timetable for selected day

diff --git a/src/routes/route/[id]/timetable/[stop]/[direction]/+page.ts b/src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.ts similarity index 84% rename from src/routes/route/[id]/timetable/[stop]/[direction]/+page.ts rename to src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.ts index 5badee7..2cbdd07 100644 --- a/src/routes/route/[id]/timetable/[stop]/[direction]/+page.ts +++ b/src/routes/route/[routeCode]/timetable/[stop]/[direction]/+page.ts @@ -2,7 +2,7 @@ import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params }) => { return { - routeId: params.id, + routeCode: params.routeCode, stopId: params.stop, directionId: params.direction, }; From bdf6fec1dda46f0ad364e54640a7f36e125acd77 Mon Sep 17 00:00:00 2001 From: michaeljwalla Date: Sun, 16 Aug 2026 22:41:49 -0500 Subject: [PATCH 17/20] some more work to align cached/live state. also surfaced a small bug in aggie_spirit.ts --- src/lib/data/structure/aggie_spirit.ts | 2 +- src/lib/managers/connectivity.manager.svelte.ts | 4 +++- src/routes/route/[routeCode]/+page.svelte | 8 ++++---- src/routes/route/[routeCode]/OfflineStopRow.svelte | 6 +----- src/routes/route/[routeCode]/StopRow.svelte | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/lib/data/structure/aggie_spirit.ts b/src/lib/data/structure/aggie_spirit.ts index 5d05273..74c34c5 100644 --- a/src/lib/data/structure/aggie_spirit.ts +++ b/src/lib/data/structure/aggie_spirit.ts @@ -179,7 +179,7 @@ export const useASStopEstimate = ( queryFn: async () => { const stopEstimates = apiStopEstimateQuery.data!; - return stopEstimates.routeDirectionTimes[0].nextDeparts.map((e) => { + return (stopEstimates.routeDirectionTimes[0]?.nextDeparts ?? []).map((e) => { return { dataSource: DataSource.AGGIE_SPIRIT, estimatedTime: e.estimatedDepartTimeUtc diff --git a/src/lib/managers/connectivity.manager.svelte.ts b/src/lib/managers/connectivity.manager.svelte.ts index e9a7554..4c91d15 100644 --- a/src/lib/managers/connectivity.manager.svelte.ts +++ b/src/lib/managers/connectivity.manager.svelte.ts @@ -101,7 +101,9 @@ class ConnectivityManager { if (!this.client) return; this.isRetrying = true; try { - await this.client.refetchQueries({ predicate: (q) => q.state.status === 'error' }); + await this.client.refetchQueries({ + predicate: (q) => q.state.status === 'error' && !!q.meta?.network, + }); } finally { this.isRetrying = false; if (this.isError) this.tryEnqueueReconnect(Date.now()); diff --git a/src/routes/route/[routeCode]/+page.svelte b/src/routes/route/[routeCode]/+page.svelte index 7312753..f3f3e8e 100644 --- a/src/routes/route/[routeCode]/+page.svelte +++ b/src/routes/route/[routeCode]/+page.svelte @@ -22,8 +22,9 @@ const routes = useRoutes(); const route = $derived(routes.data?.find((r) => r.routeCode === data.routeCode) ?? null); + const isValidated = $derived(route && connectivityManager.validate(route)); - const alerts = useAlerts(() => ({ route })); + const alerts = $derived(isValidated ? useAlerts(() => ({ route })) : null); let isFavorite = $state(false); const selectedDirection = $derived( route?.directions.find((d) => d.id === mapManager.selectedDirectionId) ?? null, @@ -43,7 +44,6 @@ mapManager.selectedDirectionId = route?.directions[0].id ?? ''; }); }); - function onClose() { goto('/'); mapManager.setSelectedRoute(null); @@ -82,7 +82,7 @@ class="rounded-full" onclick={() => goto(`/route/${route?.routeCode}/alerts`)} > - {#if alerts.data?.length ?? 0 > 0} + {#if alerts?.data?.length ?? 0 > 0} {:else} @@ -105,7 +105,7 @@ {#if selectedDirection && route}
{#each selectedDirection?.stops ?? [] as stop, i (`${stop.id}-${selectedDirection?.id}-${i}`)} - {@const Row = connectivityManager.validate(route) ? StopRow : OfflineStopRow} + {@const Row = isValidated ? StopRow : OfflineStopRow} {@const isLast = i === (selectedDirection?.stops.length ?? 0) - 1} {@const altDirection = isLast ? route.directions.length > 1 diff --git a/src/routes/route/[routeCode]/OfflineStopRow.svelte b/src/routes/route/[routeCode]/OfflineStopRow.svelte index ae4c745..75f35a5 100644 --- a/src/routes/route/[routeCode]/OfflineStopRow.svelte +++ b/src/routes/route/[routeCode]/OfflineStopRow.svelte @@ -1,11 +1,7 @@