From c18650f6878321990e8303104497e51c2eed65e8 Mon Sep 17 00:00:00 2001 From: Ilya Titov Date: Wed, 28 Jan 2026 11:03:05 +0000 Subject: [PATCH 1/5] Add subprotocol check to the client --- api/http/utils.ts | 6 +- core/client.ts | 15 +++++ core/lib/http.ts | 5 ++ core/messages/index.ts | 1 + core/messages/update-client/en.ts | 6 ++ core/pages/index.ts | 3 + core/pages/update-client.ts | 12 ++++ core/router.ts | 67 ++++++++++--------- core/test/pages/update-client.test.ts | 22 ++++++ server/lib/http.ts | 20 +++++- server/test/auth.test.ts | 17 +++++ web/.size-limit.json | 2 +- web/main/main.svelte | 3 + web/pages/update-client.svelte | 21 ++++++ web/stores/url-router.ts | 1 + .../pages/update-client.stories.svelte | 39 +++++++++++ 16 files changed, 207 insertions(+), 33 deletions(-) create mode 100644 core/messages/update-client/en.ts create mode 100644 core/pages/update-client.ts create mode 100644 core/test/pages/update-client.test.ts create mode 100644 web/pages/update-client.svelte create mode 100644 web/stories/pages/update-client.stories.svelte diff --git a/api/http/utils.ts b/api/http/utils.ts index 34717066..b96c51f3 100644 --- a/api/http/utils.ts +++ b/api/http/utils.ts @@ -1,3 +1,5 @@ +import { SUBPROTOCOL } from '../index.ts' + export function isObject(body: unknown): body is object { return typeof body === 'object' && body !== null } @@ -54,10 +56,12 @@ export async function fetchJSON( body: JSON.stringify(body), credentials: 'include', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-Subprotocol': String(SUBPROTOCOL) }, method }) + if (opts.response) opts.response(response) return response as HTTPResponse } diff --git a/core/client.ts b/core/client.ts index a0c67cb7..942a0e38 100644 --- a/core/client.ts +++ b/core/client.ts @@ -51,6 +51,13 @@ function getServer(): ClientOptions['server'] { let prevClient: CrossTabClient | undefined export const client = atom() +export const isClientUpdateRequired = atom(false) + +/* node:coverage disable */ +export function handleClientUpdateRequired(): void { + isClientUpdateRequired.set(true) +} +/* node:coverage enable */ onEnvironment(({ logStoreCreator }) => { let unbindUser = effect( @@ -71,6 +78,14 @@ onEnvironment(({ logStoreCreator }) => { encryptActions(logux, key, { ignore: [deleteUser.type] }) + + /* node:coverage disable */ + logux.node.on('error', error => { + if (error.type === 'wrong-subprotocol') { + handleClientUpdateRequired() + } + }) + /* node:coverage enable */ logux.start(connect) prevClient = logux client.set(logux) diff --git a/core/lib/http.ts b/core/lib/http.ts index 79108fcd..dd836d6e 100644 --- a/core/lib/http.ts +++ b/core/lib/http.ts @@ -6,6 +6,7 @@ import { HTTPStatusError, UserFacingError } from '../errors.ts' +import { handleClientUpdateRequired } from '../index.ts' /** * Takes fetch() wrapper from `@slowreader/api/http` and do the request @@ -31,6 +32,10 @@ export async function checkErrors( if (!response.ok) { let text = await response.text() if (response.status === 400 && text !== 'Invalid request') { + let action = response.headers.get('X-Client-Action') + if (action === 'update-client') { + handleClientUpdateRequired() + } throw new UserFacingError(text) } else { throw new HTTPStatusError( diff --git a/core/messages/index.ts b/core/messages/index.ts index 01849f2f..6b43364c 100644 --- a/core/messages/index.ts +++ b/core/messages/index.ts @@ -12,4 +12,5 @@ export * from './post/en.ts' export * from './profile/en.ts' export * from './refresh/en.ts' export * from './settings/en.ts' +export * from './update-client/en.ts' export * from './welcome/en.ts' diff --git a/core/messages/update-client/en.ts b/core/messages/update-client/en.ts new file mode 100644 index 00000000..7bc8cdc0 --- /dev/null +++ b/core/messages/update-client/en.ts @@ -0,0 +1,6 @@ +import { i18n } from '../../i18n.ts' + +export const updateClientMessages = i18n('updateClient', { + pageTitle: 'Update Client', + updateButton: 'Update Client' +}) diff --git a/core/pages/index.ts b/core/pages/index.ts index d887bba0..9889fe1c 100644 --- a/core/pages/index.ts +++ b/core/pages/index.ts @@ -14,6 +14,7 @@ import { importPage } from './import.ts' import { profilePage } from './profile.ts' import { signupPage } from './signup.ts' import { startPage } from './start.ts' +import { updateClientPage } from './update-client.ts' export type { AboutPage } from './about.ts' export type { AddPage } from './add.ts' @@ -26,6 +27,7 @@ export type { ImportPage } from './import.ts' export type { ProfilePage } from './profile.ts' export type { SignupPage } from './signup.ts' export type { StartPage } from './start.ts' +export type { UpdateClientPage } from './update-client.ts' export const pages = { about: aboutPage, @@ -45,6 +47,7 @@ export const pages = { signup: signupPage, slow: slowPage, start: startPage, + updateClient: updateClientPage, welcome: createSimplePage('welcome') } satisfies { [Name in RouteName]: Name extends 'fast' | 'slow' diff --git a/core/pages/update-client.ts b/core/pages/update-client.ts new file mode 100644 index 00000000..40ff64ab --- /dev/null +++ b/core/pages/update-client.ts @@ -0,0 +1,12 @@ +import { createPage } from './common.ts' + +export const updateClientPage = createPage('updateClient', () => { + return { + exit() {}, + message: `Hi there 👋 It looks like you’re using an outdated client version. Please + update the app.`, + params: {} + } +}) + +export type UpdateClientPage = ReturnType diff --git a/core/router.ts b/core/router.ts index a5bcc59e..0a7071fd 100644 --- a/core/router.ts +++ b/core/router.ts @@ -1,5 +1,6 @@ import { atom, computed, effect, type ReadableAtom } from 'nanostores' +import { isClientUpdateRequired } from './client.ts' import { getEnvironment, onEnvironment } from './environment.ts' import { NotFoundError } from './errors.ts' import { userId } from './settings.ts' @@ -28,6 +29,7 @@ export interface Routes { from?: number } start: {} + updateClient: {} welcome: {} } @@ -96,7 +98,7 @@ export type OtherName = | (typeof FEED_ROUTES)[number] | (typeof SETTINGS_ROUTES)[number] -const GUEST = new Set(['signin', 'start']) +const GUEST = new Set(['signin', 'start', 'updateClient']) const BOTH = new Set(['notFound', 'signup']) @@ -151,39 +153,44 @@ export function parsePopups(hash: string): PopupRoute[] { } onEnvironment(({ baseRouter }) => { - return effect([baseRouter, userId], (route, user) => { - let popups = user && route ? parsePopups(route.hash) : [] - let nextRoute: Route - try { - if (!route) { - nextRoute = open('notFound') - } else if (!user && !GUEST.has(route.route) && !BOTH.has(route.route)) { - nextRoute = open('start') - } else if (user && GUEST.has(route.route)) { - nextRoute = redirect(open('home')) - } else if (route.route === 'fast' || route.route === 'slow') { - nextRoute = { - params: { - ...route.params, - from: validateNumber(route.params.from) - }, - popups, - route: route.route + return effect( + [baseRouter, userId, isClientUpdateRequired], + (route, user, isUpdateRequired) => { + let popups = user && route ? parsePopups(route.hash) : [] + let nextRoute: Route + try { + if (!route) { + nextRoute = open('notFound') + } else if (isUpdateRequired) { + nextRoute = redirect(open('updateClient')) + } else if (!user && !GUEST.has(route.route) && !BOTH.has(route.route)) { + nextRoute = open('start') + } else if (user && GUEST.has(route.route)) { + nextRoute = redirect(open('home')) + } else if (route.route === 'fast' || route.route === 'slow') { + nextRoute = { + params: { + ...route.params, + from: validateNumber(route.params.from) + }, + popups, + route: route.route + } + } else { + nextRoute = { params: route.params, popups, route: route.route } + } + } catch (e) { + if (e instanceof NotFoundError) { + nextRoute = open('notFound') + } else { + throw e } - } else { - nextRoute = { params: route.params, popups, route: route.route } } - } catch (e) { - if (e instanceof NotFoundError) { - nextRoute = open('notFound') - } else { - throw e + if (JSON.stringify(router.get()) !== JSON.stringify(nextRoute)) { + router.set(nextRoute) } } - if (JSON.stringify(router.get()) !== JSON.stringify(nextRoute)) { - router.set(nextRoute) - } - }) + ) }) export function isOtherRoute(route: Route): boolean { diff --git a/core/test/pages/update-client.test.ts b/core/test/pages/update-client.test.ts new file mode 100644 index 00000000..75202233 --- /dev/null +++ b/core/test/pages/update-client.test.ts @@ -0,0 +1,22 @@ +import { equal } from 'node:assert' +import { afterEach, beforeEach, test } from 'node:test' + +import { isClientUpdateRequired } from '../../index.ts' +import { cleanClientTest, enableClientTest, openPage } from '../utils.ts' + +beforeEach(() => { + enableClientTest() + isClientUpdateRequired.set(true) +}) + +afterEach(async () => { + await cleanClientTest() +}) + +test('shows update message', () => { + let page = openPage({ + params: {}, + route: 'updateClient' + }) + equal(page.message.includes('update the app'), true) +}) diff --git a/server/lib/http.ts b/server/lib/http.ts index 8b38d7f5..5c4d0df6 100644 --- a/server/lib/http.ts +++ b/server/lib/http.ts @@ -37,7 +37,7 @@ function allowCors(res: ServerResponse, origin: string): void { 'Access-Control-Allow-Methods', 'OPTIONS, POST, GET, PUT, DELETE' ) - res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Subprotocol') } const LOCALHOST = /:\/\/localhost:/ @@ -67,6 +67,24 @@ export function jsonApi( allowCors(res, req.headers.origin) } } + + if (req.headers['x-subprotocol'] && server.options.minSubprotocol) { + let clientSubprotocol = Number(req.headers['x-subprotocol']) + + if ( + isNaN(clientSubprotocol) || + clientSubprotocol < server.options.minSubprotocol + ) { + res.writeHead(400, { + 'Access-Control-Expose-Headers': 'X-Client-Action', + 'Content-Type': 'text/plain', + 'X-Client-Action': 'update-client' + }) + res.end('Old client. Please update.') + return true + } + } + if (req.method === 'OPTIONS') { res.writeHead(200) res.end() diff --git a/server/test/auth.test.ts b/server/test/auth.test.ts index 450329cb..efa56f08 100644 --- a/server/test/auth.test.ts +++ b/server/test/auth.test.ts @@ -253,3 +253,20 @@ test('supports CORS', async () => { 'https://dev.slowreader.app' ) }) + +test('rejects old clients', async () => { + server = buildTestServer() + server.options.minSubprotocol = 2 + + let response = await server.fetch('/users/1', { + headers: { + 'Content-Type': 'application/json', + 'X-Subprotocol': '0' + }, + method: 'POST' + }) + equal(response.status, 400) + equal(response.headers.get('X-Client-Action'), 'update-client') + let text = await response.text() + equal(text, 'Old client. Please update.') +}) diff --git a/web/.size-limit.json b/web/.size-limit.json index 46f2ef92..0f6ddce4 100644 --- a/web/.size-limit.json +++ b/web/.size-limit.json @@ -16,7 +16,7 @@ "!dist/assets/playpen-sans-*.woff2", "!dist/assets/*-{italic,math,latin-ext}-*.woff2" ], - "limit": "185 KB" + "limit": "190 KB" }, { "name": "Core scripts to execute", diff --git a/web/main/main.svelte b/web/main/main.svelte index 0e387a0f..7bfd468b 100644 --- a/web/main/main.svelte +++ b/web/main/main.svelte @@ -23,6 +23,7 @@ import ProfilePage from '../pages/profile.svelte' import SignupPage from '../pages/signup.svelte' import StartPage from '../pages/start.svelte' + import UpdateClientPage from '../pages/update-client.svelte' import FeedPopup from '../popups/feed.svelte' import LoadingPopup from '../popups/loading.svelte' import NotFoundPopup from '../popups/not-found.svelte' @@ -85,6 +86,8 @@ {:else if $currentPage.route === 'import'} +{:else if $currentPage.route === 'updateClient'} + {:else} {$currentPage.route} diff --git a/web/pages/update-client.svelte b/web/pages/update-client.svelte new file mode 100644 index 00000000..5fd8bc8b --- /dev/null +++ b/web/pages/update-client.svelte @@ -0,0 +1,21 @@ + + + +

+ {message} +

+ +
diff --git a/web/stores/url-router.ts b/web/stores/url-router.ts index 6e3d2af6..35e55288 100644 --- a/web/stores/url-router.ts +++ b/web/stores/url-router.ts @@ -34,6 +34,7 @@ export const pathRouter = createRouter({ signup: '/signup', slow: '/slow/:feed?', start: '/start', + updateClient: '/update-client', welcome: '/welcome' }) diff --git a/web/stories/pages/update-client.stories.svelte b/web/stories/pages/update-client.stories.svelte new file mode 100644 index 00000000..8f5f6805 --- /dev/null +++ b/web/stories/pages/update-client.stories.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + From f35cc3e5b40aeceb37876b7aedc2bedfc2a9ab61 Mon Sep 17 00:00:00 2001 From: Ilya Titov Date: Thu, 29 Jan 2026 08:45:12 +0000 Subject: [PATCH 2/5] Replace custom headers with response body for subprotocol errors --- api/index.ts | 1 + core/client.ts | 8 ++++---- core/lib/http.ts | 9 ++++----- core/router.ts | 4 ++-- core/test/pages/update-client.test.ts | 4 ++-- server/lib/http.ts | 10 ++-------- server/test/auth.test.ts | 6 +++--- 7 files changed, 18 insertions(+), 24 deletions(-) diff --git a/api/index.ts b/api/index.ts index 235ef378..5763b4b1 100644 --- a/api/index.ts +++ b/api/index.ts @@ -2,6 +2,7 @@ * Client’s protocol version */ export const SUBPROTOCOL = 0 +export const SUBPROTOCOL_ERROR_MESSAGE = 'Outdated client' /** * Project’s version to use in UI. diff --git a/core/client.ts b/core/client.ts index 942a0e38..c8c77d32 100644 --- a/core/client.ts +++ b/core/client.ts @@ -51,11 +51,11 @@ function getServer(): ClientOptions['server'] { let prevClient: CrossTabClient | undefined export const client = atom() -export const isClientUpdateRequired = atom(false) +export const isOutdatedClient = atom(false) /* node:coverage disable */ -export function handleClientUpdateRequired(): void { - isClientUpdateRequired.set(true) +export function handleOutdatedClient(): void { + isOutdatedClient.set(true) } /* node:coverage enable */ @@ -82,7 +82,7 @@ onEnvironment(({ logStoreCreator }) => { /* node:coverage disable */ logux.node.on('error', error => { if (error.type === 'wrong-subprotocol') { - handleClientUpdateRequired() + handleOutdatedClient() } }) /* node:coverage enable */ diff --git a/core/lib/http.ts b/core/lib/http.ts index dd836d6e..3eca93cb 100644 --- a/core/lib/http.ts +++ b/core/lib/http.ts @@ -1,4 +1,4 @@ -import type { Requester } from '@slowreader/api' +import { type Requester, SUBPROTOCOL_ERROR_MESSAGE } from '@slowreader/api' import { getEnvironment } from '../environment.ts' import { @@ -6,7 +6,7 @@ import { HTTPStatusError, UserFacingError } from '../errors.ts' -import { handleClientUpdateRequired } from '../index.ts' +import { handleOutdatedClient } from '../index.ts' /** * Takes fetch() wrapper from `@slowreader/api/http` and do the request @@ -32,9 +32,8 @@ export async function checkErrors( if (!response.ok) { let text = await response.text() if (response.status === 400 && text !== 'Invalid request') { - let action = response.headers.get('X-Client-Action') - if (action === 'update-client') { - handleClientUpdateRequired() + if (text === SUBPROTOCOL_ERROR_MESSAGE) { + handleOutdatedClient() } throw new UserFacingError(text) } else { diff --git a/core/router.ts b/core/router.ts index 0a7071fd..2018edef 100644 --- a/core/router.ts +++ b/core/router.ts @@ -1,6 +1,6 @@ import { atom, computed, effect, type ReadableAtom } from 'nanostores' -import { isClientUpdateRequired } from './client.ts' +import { isOutdatedClient } from './client.ts' import { getEnvironment, onEnvironment } from './environment.ts' import { NotFoundError } from './errors.ts' import { userId } from './settings.ts' @@ -154,7 +154,7 @@ export function parsePopups(hash: string): PopupRoute[] { onEnvironment(({ baseRouter }) => { return effect( - [baseRouter, userId, isClientUpdateRequired], + [baseRouter, userId, isOutdatedClient], (route, user, isUpdateRequired) => { let popups = user && route ? parsePopups(route.hash) : [] let nextRoute: Route diff --git a/core/test/pages/update-client.test.ts b/core/test/pages/update-client.test.ts index 75202233..3a7742c8 100644 --- a/core/test/pages/update-client.test.ts +++ b/core/test/pages/update-client.test.ts @@ -1,12 +1,12 @@ import { equal } from 'node:assert' import { afterEach, beforeEach, test } from 'node:test' -import { isClientUpdateRequired } from '../../index.ts' +import { isOutdatedClient } from '../../index.ts' import { cleanClientTest, enableClientTest, openPage } from '../utils.ts' beforeEach(() => { enableClientTest() - isClientUpdateRequired.set(true) + isOutdatedClient.set(true) }) afterEach(async () => { diff --git a/server/lib/http.ts b/server/lib/http.ts index 5c4d0df6..f4500135 100644 --- a/server/lib/http.ts +++ b/server/lib/http.ts @@ -1,5 +1,5 @@ import type { BaseServer } from '@logux/server' -import type { Endpoint } from '@slowreader/api' +import { type Endpoint, SUBPROTOCOL_ERROR_MESSAGE } from '@slowreader/api' import type { IncomingMessage, ServerResponse } from 'node:http' import { config } from './config.ts' @@ -75,13 +75,7 @@ export function jsonApi( isNaN(clientSubprotocol) || clientSubprotocol < server.options.minSubprotocol ) { - res.writeHead(400, { - 'Access-Control-Expose-Headers': 'X-Client-Action', - 'Content-Type': 'text/plain', - 'X-Client-Action': 'update-client' - }) - res.end('Old client. Please update.') - return true + return badRequest(res, SUBPROTOCOL_ERROR_MESSAGE) } } diff --git a/server/test/auth.test.ts b/server/test/auth.test.ts index efa56f08..3d5a7175 100644 --- a/server/test/auth.test.ts +++ b/server/test/auth.test.ts @@ -4,7 +4,8 @@ import { setPassword, signIn, signOut, - signUp + signUp, + SUBPROTOCOL_ERROR_MESSAGE } from '@slowreader/api' import { eq } from 'drizzle-orm' import { deepEqual, equal, notEqual, ok } from 'node:assert/strict' @@ -266,7 +267,6 @@ test('rejects old clients', async () => { method: 'POST' }) equal(response.status, 400) - equal(response.headers.get('X-Client-Action'), 'update-client') let text = await response.text() - equal(text, 'Old client. Please update.') + equal(text, SUBPROTOCOL_ERROR_MESSAGE) }) From f342e915dae02c81b52f3c173195933ea328895d Mon Sep 17 00:00:00 2001 From: Ilya Titov Date: Thu, 29 Jan 2026 11:06:32 +0000 Subject: [PATCH 3/5] Update page design --- core/messages/update-client/en.ts | 6 ++-- core/pages/update-client.ts | 8 +++-- core/router.ts | 4 +-- core/test/pages/update-client.test.ts | 5 +++- web/pages/update-client.svelte | 30 ++++++++++++++----- .../pages/update-client.stories.svelte | 21 ------------- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/core/messages/update-client/en.ts b/core/messages/update-client/en.ts index 7bc8cdc0..dd13f3b3 100644 --- a/core/messages/update-client/en.ts +++ b/core/messages/update-client/en.ts @@ -1,6 +1,8 @@ import { i18n } from '../../i18n.ts' export const updateClientMessages = i18n('updateClient', { - pageTitle: 'Update Client', - updateButton: 'Update Client' + pageText: + 'You’re using an outdated client version. Please update the app to continue.', + pageTitle: 'Update Your App', + updateButton: 'Update Now' }) diff --git a/core/pages/update-client.ts b/core/pages/update-client.ts index 40ff64ab..7fd1a2d5 100644 --- a/core/pages/update-client.ts +++ b/core/pages/update-client.ts @@ -1,10 +1,14 @@ +import { isOutdatedClient } from '../client.ts' import { createPage } from './common.ts' export const updateClientPage = createPage('updateClient', () => { + function handleUpdateClient(): void { + isOutdatedClient.set(false) + } + return { exit() {}, - message: `Hi there 👋 It looks like you’re using an outdated client version. Please - update the app.`, + handleUpdateClient, params: {} } }) diff --git a/core/router.ts b/core/router.ts index 2018edef..1855a446 100644 --- a/core/router.ts +++ b/core/router.ts @@ -155,13 +155,13 @@ export function parsePopups(hash: string): PopupRoute[] { onEnvironment(({ baseRouter }) => { return effect( [baseRouter, userId, isOutdatedClient], - (route, user, isUpdateRequired) => { + (route, user, outdatedClient) => { let popups = user && route ? parsePopups(route.hash) : [] let nextRoute: Route try { if (!route) { nextRoute = open('notFound') - } else if (isUpdateRequired) { + } else if (outdatedClient) { nextRoute = redirect(open('updateClient')) } else if (!user && !GUEST.has(route.route) && !BOTH.has(route.route)) { nextRoute = open('start') diff --git a/core/test/pages/update-client.test.ts b/core/test/pages/update-client.test.ts index 3a7742c8..7641cf28 100644 --- a/core/test/pages/update-client.test.ts +++ b/core/test/pages/update-client.test.ts @@ -18,5 +18,8 @@ test('shows update message', () => { params: {}, route: 'updateClient' }) - equal(page.message.includes('update the app'), true) + + page.handleUpdateClient() + + equal(isOutdatedClient.get(), false) }) diff --git a/web/pages/update-client.svelte b/web/pages/update-client.svelte index 5fd8bc8b..401fcde9 100644 --- a/web/pages/update-client.svelte +++ b/web/pages/update-client.svelte @@ -6,16 +6,30 @@ import Button from '../ui/button.svelte' import ThinPage from '../ui/thin-page.svelte' + import Stack from '../ui/stack.svelte' + import PageIcon from '../ui/page-icon.svelte' + import Title from '../ui/title.svelte' + + import { mdiSyncOff } from '@mdi/js' let { page }: { page: UpdateClientPage } = $props() - let { message } = $derived(page) - -

- {message} -

- + + + + {$t.pageTitle} +

{$t.pageText}

+ +
+ + diff --git a/web/stories/pages/update-client.stories.svelte b/web/stories/pages/update-client.stories.svelte index 8f5f6805..54fb7821 100644 --- a/web/stories/pages/update-client.stories.svelte +++ b/web/stories/pages/update-client.stories.svelte @@ -16,24 +16,3 @@ - - - - - - - - - - - - From 1776132aab7ada6a3999f8bff1ba74e8b4f53e64 Mon Sep 17 00:00:00 2001 From: Ilya Titov Date: Fri, 30 Jan 2026 14:20:02 +0000 Subject: [PATCH 4/5] Remove handleOutdatedClient function --- core/client.ts | 8 +----- core/lib/http.ts | 4 +-- core/router.ts | 66 ++++++++++++++++++++++-------------------------- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/core/client.ts b/core/client.ts index c8c77d32..c43ddad1 100644 --- a/core/client.ts +++ b/core/client.ts @@ -53,12 +53,6 @@ let prevClient: CrossTabClient | undefined export const client = atom() export const isOutdatedClient = atom(false) -/* node:coverage disable */ -export function handleOutdatedClient(): void { - isOutdatedClient.set(true) -} -/* node:coverage enable */ - onEnvironment(({ logStoreCreator }) => { let unbindUser = effect( [userId, hasPassword, encryptionKey], @@ -82,7 +76,7 @@ onEnvironment(({ logStoreCreator }) => { /* node:coverage disable */ logux.node.on('error', error => { if (error.type === 'wrong-subprotocol') { - handleOutdatedClient() + isOutdatedClient.set(true) } }) /* node:coverage enable */ diff --git a/core/lib/http.ts b/core/lib/http.ts index 3eca93cb..86f949b3 100644 --- a/core/lib/http.ts +++ b/core/lib/http.ts @@ -6,7 +6,7 @@ import { HTTPStatusError, UserFacingError } from '../errors.ts' -import { handleOutdatedClient } from '../index.ts' +import { isOutdatedClient } from '../index.ts' /** * Takes fetch() wrapper from `@slowreader/api/http` and do the request @@ -33,7 +33,7 @@ export async function checkErrors( let text = await response.text() if (response.status === 400 && text !== 'Invalid request') { if (text === SUBPROTOCOL_ERROR_MESSAGE) { - handleOutdatedClient() + isOutdatedClient.set(true) } throw new UserFacingError(text) } else { diff --git a/core/router.ts b/core/router.ts index 1855a446..a04e78cf 100644 --- a/core/router.ts +++ b/core/router.ts @@ -1,6 +1,5 @@ import { atom, computed, effect, type ReadableAtom } from 'nanostores' -import { isOutdatedClient } from './client.ts' import { getEnvironment, onEnvironment } from './environment.ts' import { NotFoundError } from './errors.ts' import { userId } from './settings.ts' @@ -98,7 +97,7 @@ export type OtherName = | (typeof FEED_ROUTES)[number] | (typeof SETTINGS_ROUTES)[number] -const GUEST = new Set(['signin', 'start', 'updateClient']) +const GUEST = new Set(['signin', 'start']) const BOTH = new Set(['notFound', 'signup']) @@ -153,44 +152,39 @@ export function parsePopups(hash: string): PopupRoute[] { } onEnvironment(({ baseRouter }) => { - return effect( - [baseRouter, userId, isOutdatedClient], - (route, user, outdatedClient) => { - let popups = user && route ? parsePopups(route.hash) : [] - let nextRoute: Route - try { - if (!route) { - nextRoute = open('notFound') - } else if (outdatedClient) { - nextRoute = redirect(open('updateClient')) - } else if (!user && !GUEST.has(route.route) && !BOTH.has(route.route)) { - nextRoute = open('start') - } else if (user && GUEST.has(route.route)) { - nextRoute = redirect(open('home')) - } else if (route.route === 'fast' || route.route === 'slow') { - nextRoute = { - params: { - ...route.params, - from: validateNumber(route.params.from) - }, - popups, - route: route.route - } - } else { - nextRoute = { params: route.params, popups, route: route.route } - } - } catch (e) { - if (e instanceof NotFoundError) { - nextRoute = open('notFound') - } else { - throw e + return effect([baseRouter, userId], (route, user) => { + let popups = user && route ? parsePopups(route.hash) : [] + let nextRoute: Route + try { + if (!route) { + nextRoute = open('notFound') + } else if (!user && !GUEST.has(route.route) && !BOTH.has(route.route)) { + nextRoute = open('start') + } else if (user && GUEST.has(route.route)) { + nextRoute = redirect(open('home')) + } else if (route.route === 'fast' || route.route === 'slow') { + nextRoute = { + params: { + ...route.params, + from: validateNumber(route.params.from) + }, + popups, + route: route.route } + } else { + nextRoute = { params: route.params, popups, route: route.route } } - if (JSON.stringify(router.get()) !== JSON.stringify(nextRoute)) { - router.set(nextRoute) + } catch (e) { + if (e instanceof NotFoundError) { + nextRoute = open('notFound') + } else { + throw e } } - ) + if (JSON.stringify(router.get()) !== JSON.stringify(nextRoute)) { + router.set(nextRoute) + } + }) }) export function isOtherRoute(route: Route): boolean { From 25c2970891167b4e933dedb92232adc662afba1e Mon Sep 17 00:00:00 2001 From: Ilya Titov Date: Fri, 30 Jan 2026 14:28:13 +0000 Subject: [PATCH 5/5] Refactor updateClientPage --- core/pages/update-client.ts | 5 +++-- core/test/pages/update-client.test.ts | 17 ++++++++++------- web/main/main.svelte | 6 ++++-- web/pages/update-client.svelte | 9 ++++----- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/core/pages/update-client.ts b/core/pages/update-client.ts index 7fd1a2d5..64731134 100644 --- a/core/pages/update-client.ts +++ b/core/pages/update-client.ts @@ -1,10 +1,11 @@ -import { isOutdatedClient } from '../client.ts' import { createPage } from './common.ts' export const updateClientPage = createPage('updateClient', () => { + /* node:coverage disable */ function handleUpdateClient(): void { - isOutdatedClient.set(false) + location.reload() } + /* node:coverage enable */ return { exit() {}, diff --git a/core/test/pages/update-client.test.ts b/core/test/pages/update-client.test.ts index 7641cf28..0bc2899e 100644 --- a/core/test/pages/update-client.test.ts +++ b/core/test/pages/update-client.test.ts @@ -1,25 +1,28 @@ import { equal } from 'node:assert' import { afterEach, beforeEach, test } from 'node:test' -import { isOutdatedClient } from '../../index.ts' -import { cleanClientTest, enableClientTest, openPage } from '../utils.ts' +import { currentPage } from '../../index.ts' +import { + cleanClientTest, + enableClientTest, + openPage, + setBaseTestRoute +} from '../utils.ts' beforeEach(() => { enableClientTest() - isOutdatedClient.set(true) + setBaseTestRoute({ params: {}, route: 'updateClient' }) }) afterEach(async () => { await cleanClientTest() }) -test('shows update message', () => { +test('shows update client page', () => { let page = openPage({ params: {}, route: 'updateClient' }) - page.handleUpdateClient() - - equal(isOutdatedClient.get(), false) + equal(currentPage.get().route, page.route) }) diff --git a/web/main/main.svelte b/web/main/main.svelte index 7bfd468b..5e12f7af 100644 --- a/web/main/main.svelte +++ b/web/main/main.svelte @@ -2,8 +2,10 @@ import { busy, currentPage, + isOutdatedClient, layoutType, notFound, + pages, popupsStatus, signOut, subscribeUntil, @@ -60,6 +62,8 @@ {#if !globalLoader} {/if} +{:else if $isOutdatedClient} + {:else if $currentPage.route === 'notFound'} {:else if $currentPage.route === 'fast'} @@ -86,8 +90,6 @@ {:else if $currentPage.route === 'import'} -{:else if $currentPage.route === 'updateClient'} - {:else} {$currentPage.route} diff --git a/web/pages/update-client.svelte b/web/pages/update-client.svelte index 401fcde9..bb105ba2 100644 --- a/web/pages/update-client.svelte +++ b/web/pages/update-client.svelte @@ -1,17 +1,16 @@ @@ -20,7 +19,7 @@ {$t.pageTitle}

{$t.pageText}

-