aboutsummaryrefslogtreecommitdiff
path: root/src/lib/shared
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-10-18 21:26:24 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-10-18 21:26:24 +0200
commit031f99e626f52ea7baac9155b0cbe6e4fcfbfb31 (patch)
tree5c6851213527ce7c8f5945c56a11448c7e8fdeac /src/lib/shared
parent66939bab93cdcb9b7732c45d7bc3b99b6951fe15 (diff)
feat: poll server data so shared pages stay near-live
Refetch the current page every 15s while the tab is visible, gated by a pure refresh policy: edit pages, hidden or offline tabs and in-flight navigations are left alone. Replaces refresh-on-refocus with auto-refresh.
Diffstat (limited to 'src/lib/shared')
-rw-r--r--src/lib/shared/refresh-policy.test.ts43
-rw-r--r--src/lib/shared/refresh-policy.ts31
2 files changed, 74 insertions, 0 deletions
diff --git a/src/lib/shared/refresh-policy.test.ts b/src/lib/shared/refresh-policy.test.ts
new file mode 100644
index 0000000..5cab234
--- /dev/null
+++ b/src/lib/shared/refresh-policy.test.ts
@@ -0,0 +1,43 @@
+import { describe, it, expect } from 'vitest';
+import { shouldAutoRefresh } from './refresh-policy';
+
+const idle = { visible: true, online: true, navigating: false };
+
+describe('shouldAutoRefresh', () => {
+ it('refreshes a list page when the tab is visible and idle', () => {
+ expect(shouldAutoRefresh({ routeId: '/groceries', ...idle })).toBe(true);
+ expect(shouldAutoRefresh({ routeId: '/groceries/catalog', ...idle })).toBe(
+ true,
+ );
+ expect(shouldAutoRefresh({ routeId: '/projects/[id]', ...idle })).toBe(
+ true,
+ );
+ });
+
+ it('never refreshes a page holding an in-progress edit', () => {
+ expect(shouldAutoRefresh({ routeId: '/grocery/[id]', ...idle })).toBe(
+ false,
+ );
+ expect(
+ shouldAutoRefresh({
+ routeId: '/projects/[projectId]/routine/[routineId]',
+ ...idle,
+ }),
+ ).toBe(false);
+ expect(shouldAutoRefresh({ routeId: '/login', ...idle })).toBe(false);
+ });
+
+ it('holds off while hidden, offline or mid-navigation', () => {
+ const routeId = '/groceries';
+ expect(shouldAutoRefresh({ routeId, ...idle, visible: false })).toBe(false);
+ expect(shouldAutoRefresh({ routeId, ...idle, online: false })).toBe(false);
+ expect(shouldAutoRefresh({ routeId, ...idle, navigating: true })).toBe(
+ false,
+ );
+ });
+
+ it('holds off when the route is not resolved yet', () => {
+ expect(shouldAutoRefresh({ routeId: null, ...idle })).toBe(false);
+ expect(shouldAutoRefresh({ routeId: undefined, ...idle })).toBe(false);
+ });
+});
diff --git a/src/lib/shared/refresh-policy.ts b/src/lib/shared/refresh-policy.ts
new file mode 100644
index 0000000..d79e6b7
--- /dev/null
+++ b/src/lib/shared/refresh-policy.ts
@@ -0,0 +1,31 @@
+/**
+ * Routes whose page data must not be refetched in the background: edit pages
+ * hold in-progress work, and refreshing the login form is pointless.
+ */
+export const EXCLUDED_ROUTES: Record<string, true> = {
+ '/login': true,
+ '/grocery/[id]': true,
+ '/projects/[projectId]/routine/[routineId]': true,
+};
+
+export interface RefreshConditions {
+ /** `$page.route.id`; null when no route matched yet. */
+ routeId: string | null | undefined;
+ /** Document is visible (not a backgrounded tab). */
+ visible: boolean;
+ /** Browser reports a network connection. */
+ online: boolean;
+ /** A navigation or form action is already in flight. */
+ navigating: boolean;
+}
+
+/** A background refetch may re-run the page load only if nothing is at stake. */
+export function shouldAutoRefresh({
+ routeId,
+ visible,
+ online,
+ navigating,
+}: RefreshConditions): boolean {
+ if (!routeId || routeId in EXCLUDED_ROUTES) return false;
+ return visible && online && !navigating;
+}