From eb85f1d99285711699e3df6af94d04dd95839800 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 23 Jul 2026 17:04:03 -0600 Subject: [PATCH 1/5] feat: more frequent version checks --- .changeset/more-frequent-updated-checks.md | 5 + packages/kit/src/core/config/index.spec.js | 2 +- packages/kit/src/core/config/options.js | 2 +- packages/kit/src/core/sync/write_server.js | 1 + packages/kit/src/exports/public.d.ts | 10 +- packages/kit/src/exports/vite/index.js | 1 + packages/kit/src/runtime/app/forms.js | 4 + packages/kit/src/runtime/app/state/index.js | 2 +- packages/kit/src/runtime/client/client.js | 13 +- .../remote-functions/query-live/iterator.js | 4 + .../client/remote-functions/shared.svelte.js | 5 +- .../remote-functions/shared.transport.spec.js | 28 +++- .../kit/src/runtime/client/state.svelte.js | 35 ++++- .../src/runtime/client/state.svelte.spec.js | 121 ++++++++++++++++++ packages/kit/src/runtime/server/respond.js | 8 ++ packages/kit/src/types/global-private.d.ts | 2 + packages/kit/src/types/internal.d.ts | 1 + .../kit/test/apps/basics/test/server.test.js | 5 + packages/kit/test/apps/options-3/test/test.js | 5 + packages/kit/types/index.d.ts | 12 +- 20 files changed, 239 insertions(+), 27 deletions(-) create mode 100644 .changeset/more-frequent-updated-checks.md create mode 100644 packages/kit/src/runtime/client/state.svelte.spec.js diff --git a/.changeset/more-frequent-updated-checks.md b/.changeset/more-frequent-updated-checks.md new file mode 100644 index 000000000000..667240c7920e --- /dev/null +++ b/.changeset/more-frequent-updated-checks.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': breaking +--- + +breaking: detect new deployments on every server response, tab focus, and visibility change, and default `version.pollInterval` to 1 hour diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 2123a7c5d140..314ee174deb0 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -142,7 +142,7 @@ const get_defaults = (prefix = '') => ({ }, version: { name: Date.now().toString(), - pollInterval: 0 + pollInterval: 3_600_000 } } }); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 9a40a0734f6b..c615a544afed 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -292,7 +292,7 @@ export const validate_kit_options = object({ version: object({ name: string(Date.now().toString()), - pollInterval: number(0) + pollInterval: number(3_600_000) }) }); diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 521d50ac029d..d2336eda8107 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -53,6 +53,7 @@ export const options = { )}, error }, + version: ${s(config.kit.version.name)}, version_hash: ${s(hash(config.kit.version.name))} }; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index c1f28309fa6f..5c7ffe5b9c0a 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -875,9 +875,9 @@ export interface KitConfig { }; /** * Client-side navigation can be buggy if you deploy a new version of your app while people are using it. If the code for the new page is already loaded, it may have stale content; if it isn't, the app's route manifest may point to a JavaScript file that no longer exists. - * SvelteKit helps you solve this problem through version management. + * SvelteKit helps you solve this problem through version management. The current version is included in every server response via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. * If SvelteKit encounters an error while loading the page and detects that a new version has been deployed (using the `name` specified here, which defaults to a timestamp of the build) it will fall back to traditional full-page navigation. - * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use techniques such as setting the `pollInterval` and then using `beforeNavigate`: + * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use `beforeNavigate`: * ```html * /// file: +layout.svelte * * ``` * - * If you set `pollInterval` to a non-zero value, SvelteKit will poll for new versions in the background and set the value of [`updated.current`](https://svelte.dev/docs/kit/$app-state#updated) `true` when it detects one. + * In addition to these checks, SvelteKit polls for new versions on an interval and sets [`updated.current`](https://svelte.dev/docs/kit/$app-state#updated) to `true` when it detects one. Set `pollInterval` to `0` to disable polling (the header- and event-based checks will still run). */ version?: { /** @@ -919,8 +919,8 @@ export interface KitConfig { */ name?: string; /** - * The interval in milliseconds to poll for version changes. If this is `0`, no polling occurs. - * @default 0 + * The interval in milliseconds to poll for version changes. If this is `0`, no polling occurs. SvelteKit also checks for new versions on server responses (via the `x-sveltekit-version` header) and when the tab regains focus or becomes visible, so polling is only needed for long-lived sessions on a single page. + * @default 3600000 */ pollInterval?: number; }; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 726833c3cdb0..17bf12ce6b35 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -501,6 +501,7 @@ function kit({ svelte_config }) { const define = { __SVELTEKIT_APP_DIR__: s(posixify(kit.appDir)), __SVELTEKIT_APP_VERSION__: s(kit.version.name), + __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__: s(kit.output.bundleStrategy !== 'inline'), __SVELTEKIT_EMBEDDED__: s(kit.embedded), __SVELTEKIT_FORK_PRELOADS__: s(kit.experimental.forkPreloads), __SVELTEKIT_PATHS_ASSETS__: s(kit.paths.assets), diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js index adb55c8247a3..00480ed52627 100644 --- a/packages/kit/src/runtime/app/forms.js +++ b/packages/kit/src/runtime/app/forms.js @@ -4,6 +4,7 @@ import { noop } from '../../utils/functions.js'; import { refreshAll } from './navigation.js'; import { app as client_app, applyAction, handle_error } from '../client/client.js'; import { app as server_app } from '../server/app.js'; +import { notify_version } from '../client/state.svelte.js'; export { applyAction }; @@ -201,6 +202,9 @@ export function enhance(form_element, submit = noop) { signal: controller.signal }); + // detect new deployments from the response header + notify_version(response.headers.get('x-sveltekit-version')); + if (response.status === 204) { result = { type: 'success', status: 204 }; } else { diff --git a/packages/kit/src/runtime/app/state/index.js b/packages/kit/src/runtime/app/state/index.js index 70ed0eaab425..249029bab7c1 100644 --- a/packages/kit/src/runtime/app/state/index.js +++ b/packages/kit/src/runtime/app/state/index.js @@ -58,7 +58,7 @@ export const page = BROWSER ? client_page : server_page; export const navigating = BROWSER ? client_navigating : server_navigating; /** - * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. + * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on every server response (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. * @type {{ get current(): boolean; check(): Promise; }} */ export const updated = BROWSER ? client_updated : server_updated; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a9a3950b45ff..6a3f7daddfed 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -43,7 +43,7 @@ import { validate_load_response } from '../shared.js'; import { get_message, get_status } from '../../utils/error.js'; -import { page, update, navigating, updated } from './state.svelte.js'; +import { page, update, navigating, updated, notify_version } from './state.svelte.js'; import { payload } from './payload.js'; import { add_data_suffix, add_resolution_suffix } from '../pathname.js'; import { noop_span } from '../telemetry/noop.js'; @@ -2763,13 +2763,19 @@ function _start_router() { history.scrollRestoration = 'auto'; } }); - addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { persist_state(); + } else { + // the tab just became visible — a good time to check for a new deployment + void updated.check(); } }); + addEventListener('focus', () => { + void updated.check(); + }); + // @ts-expect-error this isn't supported everywhere yet if (!navigator.connection?.saveData) { setup_preload(); @@ -3196,6 +3202,9 @@ async function load_data(url, invalid) { const fetcher = DEV ? dev_fetch : window.fetch; const res = await fetcher(data_url.href, {}); + // detect new deployments from the response header + notify_version(res.headers.get('x-sveltekit-version')); + if (!res.ok) { // error message is a JSON-stringified string which devalue can't handle at the top level // turn it into a HttpError to not call handleError on the client again (was already handled on the server) diff --git a/packages/kit/src/runtime/client/remote-functions/query-live/iterator.js b/packages/kit/src/runtime/client/remote-functions/query-live/iterator.js index 7da351d3b522..fc345ce4dae2 100644 --- a/packages/kit/src/runtime/client/remote-functions/query-live/iterator.js +++ b/packages/kit/src/runtime/client/remote-functions/query-live/iterator.js @@ -1,5 +1,6 @@ import { app_dir, base } from '$app/paths/internal/client'; import { app } from '../../client.js'; +import { notify_version } from '../../state.svelte.js'; import { handle_side_channel_response } from '../shared.svelte.js'; import * as devalue from 'devalue'; import { HttpError } from '@sveltejs/kit/internal'; @@ -26,6 +27,9 @@ export async function* create_live_iterator( signal: controller.signal }); + // detect new deployments from the response header + notify_version(response.headers.get('x-sveltekit-version')); + if (!response.ok) { const result = await response.json().catch(() => ({ type: 'error', diff --git a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js index bad25b47736c..1c6741797041 100644 --- a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js @@ -6,7 +6,7 @@ import { app, _goto, live_query_map, query_map, query_responses } from '../clien import { HttpError, Redirect } from '@sveltejs/kit/internal'; import { untrack } from 'svelte'; import { create_remote_key, split_remote_key } from '../../shared.js'; -import { navigating, page } from '../state.svelte.js'; +import { navigating, page, notify_version } from '../state.svelte.js'; /** Indicates a query function, as opposed to a query instance */ export const QUERY_FUNCTION_ID = Symbol('sveltekit.query_function_id'); @@ -113,6 +113,9 @@ export function get_remote_request_headers() { export async function remote_request(url, init) { const response = await fetch(url, init); + // detect new deployments from the response header + notify_version(response.headers.get('x-sveltekit-version')); + if (!response.ok) { const result = await response.json().catch(() => ({ type: 'error', diff --git a/packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js b/packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js index a6efa0549331..eb4456f3c711 100644 --- a/packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js +++ b/packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js @@ -16,12 +16,29 @@ vi.mock(new URL('../client.js', import.meta.url).pathname, () => ({ // Svelte state only available in a full SvelteKit runtime. vi.mock(new URL('../state.svelte.js', import.meta.url).pathname, () => ({ navigating: { current: null }, - page: { url: new URL('http://localhost/') } + page: { url: new URL('http://localhost/') }, + notify_version: () => {} })); const { remote_request } = await import('./shared.svelte.js'); const { HttpError } = await import('@sveltejs/kit/internal'); +/** + * Build a mock fetch Response. `remote_request` reads `response.headers` before + * anything else, so every mock needs a `headers` object. + * @param {Partial & { json?: () => Promise }} props + */ +function mock_response(props) { + return Promise.resolve({ + headers: new Headers(), + ok: true, + status: 200, + statusText: 'OK', + json: () => Promise.resolve({}), + ...props + }); +} + describe('remote_request transport error handling', () => { beforeEach(() => { vi.unstubAllGlobals(); @@ -29,7 +46,7 @@ describe('remote_request transport error handling', () => { test('non-OK response with JSON error body preserves status and error body', async () => { vi.stubGlobal('fetch', () => - Promise.resolve({ + mock_response({ ok: false, status: 401, statusText: 'Unauthorized', @@ -45,7 +62,7 @@ describe('remote_request transport error handling', () => { test('non-OK response with non-JSON body falls back to response.status and statusText', async () => { vi.stubGlobal('fetch', () => - Promise.resolve({ + mock_response({ ok: false, status: 503, statusText: 'Service Unavailable', @@ -63,10 +80,7 @@ describe('remote_request transport error handling', () => { const body = JSON.stringify({ type: 'result', data: null }); vi.stubGlobal('fetch', () => - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', + mock_response({ json: () => Promise.resolve(JSON.parse(body)) }) ); diff --git a/packages/kit/src/runtime/client/state.svelte.js b/packages/kit/src/runtime/client/state.svelte.js index f29bc9d3728c..f310960f2a21 100644 --- a/packages/kit/src/runtime/client/state.svelte.js +++ b/packages/kit/src/runtime/client/state.svelte.js @@ -25,16 +25,43 @@ export const updated = new (class Updated { check = async () => false; })(); -if (!DEV && BROWSER) { +/** + * Internal: mark `updated.current` as `true` if the given version differs. + * Called from the server response header path. No-op unless version checks + * are enabled (assigned below). Not exported on the public `updated` object. + * @type {(new_version: string | null) => void} + */ +export let notify_version = () => {}; + +if (!DEV && BROWSER && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { const interval = __SVELTEKIT_APP_VERSION_POLL_INTERVAL__; - /** @type {number} */ + /** @type {number | undefined} */ let timeout; + /** True while a version check request is in-flight, to prevent concurrent checks */ + let checking = false; + + /** + * Mark `updated.current` as `true` if the given version differs from the one + * the app was hydrated with. Called from the server response header path. + * Does NOT reset the poll timer — unlike `check()`, this is a passive observation + * from a single server instance's response, not an explicit version check. The + * poll timer continues on its original schedule as a backstop. + * @param {string | null} new_version + */ + notify_version = (new_version) => { + if (new_version && new_version !== version) { + updated.current = true; + } + }; + /** @type {() => Promise} */ async function check() { - window.clearTimeout(timeout); + if (checking) return updated.current; + checking = true; + window.clearTimeout(timeout); if (interval) timeout = window.setTimeout(check, interval); try { @@ -59,6 +86,8 @@ if (!DEV && BROWSER) { return new_update; } catch { return false; + } finally { + checking = false; } } diff --git a/packages/kit/src/runtime/client/state.svelte.spec.js b/packages/kit/src/runtime/client/state.svelte.spec.js new file mode 100644 index 000000000000..f16728d5cb20 --- /dev/null +++ b/packages/kit/src/runtime/client/state.svelte.spec.js @@ -0,0 +1,121 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { updated, notify_version } from './state.svelte.js'; + +// Mock `esm-env` so the version-check logic is initialised. In the test env, +// `DEV` is true which would skip the `if (!DEV && ...)` block. +vi.mock('esm-env', () => ({ + BROWSER: true, + DEV: false +})); + +vi.hoisted(() => { + vi.stubGlobal('__SVELTEKIT_APP_VERSION_CHECKS_ENABLED__', true); + vi.stubGlobal('__SVELTEKIT_APP_VERSION_FILE__', '_app/version.json'); + vi.stubGlobal('__SVELTEKIT_APP_VERSION_POLL_INTERVAL__', 0); +}); + +describe('updated', () => { + beforeEach(() => { + // reset state between tests + updated.current = false; + }); + + test('notify_version is a no-op when the version matches', () => { + // `version` is mocked as '' in the test env + notify_version(''); + expect(updated.current).toBe(false); + }); + + test('notify_version flips current to true when the version differs', () => { + notify_version(''); + expect(updated.current).toBe(true); + }); + + test('notify_version ignores null', () => { + notify_version(null); + expect(updated.current).toBe(false); + }); + + test('notify_version ignores empty string', () => { + notify_version(''); + expect(updated.current).toBe(false); + }); + + test('check() fetches version.json and flips current on mismatch', async () => { + vi.stubGlobal('fetch', () => + Promise.resolve({ + ok: true, + headers: new Headers(), + json: () => Promise.resolve({ version: '' }) + }) + ); + + const result = await updated.check(); + expect(result).toBe(true); + expect(updated.current).toBe(true); + }); + + test('check() does not flip current when version matches', async () => { + vi.stubGlobal('fetch', () => + Promise.resolve({ + ok: true, + headers: new Headers(), + json: () => Promise.resolve({ version: '' }) + }) + ); + + const result = await updated.check(); + expect(result).toBe(false); + expect(updated.current).toBe(false); + }); + + test('check() returns false on non-ok response', async () => { + vi.stubGlobal('fetch', () => + Promise.resolve({ + ok: false, + status: 404, + headers: new Headers(), + json: () => Promise.resolve({}) + }) + ); + + const result = await updated.check(); + expect(result).toBe(false); + expect(updated.current).toBe(false); + }); + + test('check() does not run concurrent checks', async () => { + /** @type {Array<{ resolve: (value: any) => void }>} */ + let resolve_queue = []; + + vi.stubGlobal( + 'fetch', + () => + new Promise((resolve) => { + resolve_queue.push({ + resolve: () => + resolve({ + ok: true, + headers: new Headers(), + json: () => Promise.resolve({ version: '' }) + }) + }); + }) + ); + + // start two checks concurrently + const p1 = updated.check(); + const p2 = updated.check(); + + // only one fetch should be in-flight + expect(resolve_queue.length).toBe(1); + + resolve_queue[0].resolve(undefined); + + await Promise.all([p1, p2]); + + // the second check should have returned early with the current value + // (which is false since the version matched) + expect(updated.current).toBe(false); + }); +}); diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index be9b86052bec..555496d37809 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -521,6 +521,10 @@ export async function internal_respond(request, options, manifest, state) { response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); } + if (!state.prerendering && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { + response.headers.set('x-sveltekit-version', options.version); + } + resolve_span.setAttributes({ 'http.response.status_code': response.status, 'http.response.body.size': @@ -559,6 +563,10 @@ export async function internal_respond(request, options, manifest, state) { if (value) headers.set(key, value); } + // preserve the version header so a 304'd data request still tells the client the current version + const version = response.headers.get('x-sveltekit-version'); + if (version) headers.set('x-sveltekit-version', version); + for (const cookie of response.headers.getSetCookie()) { headers.append('set-cookie', cookie); } diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index 1bb3e664d601..e5484bd905d8 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -6,6 +6,8 @@ declare global { const __SVELTEKIT_APP_VERSION__: string; const __SVELTEKIT_APP_VERSION_FILE__: string; const __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: number; + /** True if version checks are enabled (i.e. `bundleStrategy !== 'inline'`) */ + const __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__: boolean; /** * True if the user ran `vite dev`. This is different from `esm-env` because * it is influenced by `NODE_ENV` which can still be true during `vite preview` diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 8a2bb1bf211e..1eeb6f6dee94 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -496,6 +496,7 @@ export interface SSROptions { }): string; error(values: { message: string; status: number }): string; }; + version: string; version_hash: string; } diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 201b0c20d93f..2ec11b8ab827 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -968,6 +968,11 @@ test.describe('Miscellaneous', () => { const response = await request.get('/prerendering/中文'); expect(response.status()).toBe(200); }); + + test('sends x-sveltekit-version header on data responses', async ({ request }) => { + const response = await request.get('/__data.json'); + expect(response.headers()['x-sveltekit-version']).toBeTruthy(); + }); }); test.describe('reroute', () => { diff --git a/packages/kit/test/apps/options-3/test/test.js b/packages/kit/test/apps/options-3/test/test.js index ff0d914f1877..fe9f8a9435f0 100644 --- a/packages/kit/test/apps/options-3/test/test.js +++ b/packages/kit/test/apps/options-3/test/test.js @@ -32,4 +32,9 @@ test.describe("bundleStrategy: 'inline'", () => { test('still emits version.json', () => { expect(fs.existsSync(`${client}/_app/version.json`)).toBe(true); }); + + test('does not send x-sveltekit-version header when checks are disabled', async ({ page }) => { + const response = await page.goto('/serialization-stream'); + expect(response?.headers()['x-sveltekit-version']).toBeUndefined(); + }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index f3c28f49ed09..f8750c0743dd 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -847,9 +847,9 @@ declare module '@sveltejs/kit' { }; /** * Client-side navigation can be buggy if you deploy a new version of your app while people are using it. If the code for the new page is already loaded, it may have stale content; if it isn't, the app's route manifest may point to a JavaScript file that no longer exists. - * SvelteKit helps you solve this problem through version management. + * SvelteKit helps you solve this problem through version management. The current version is included in every server response via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. * If SvelteKit encounters an error while loading the page and detects that a new version has been deployed (using the `name` specified here, which defaults to a timestamp of the build) it will fall back to traditional full-page navigation. - * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use techniques such as setting the `pollInterval` and then using `beforeNavigate`: + * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use `beforeNavigate`: * ```html * /// file: +layout.svelte * * ``` * - * If you set `pollInterval` to a non-zero value, SvelteKit will poll for new versions in the background and set the value of [`updated.current`](https://svelte.dev/docs/kit/$app-state#updated) `true` when it detects one. + * In addition to these checks, SvelteKit polls for new versions on an interval and sets [`updated.current`](https://svelte.dev/docs/kit/$app-state#updated) to `true` when it detects one. Set `pollInterval` to `0` to disable polling (the header- and event-based checks will still run). */ version?: { /** @@ -891,8 +891,8 @@ declare module '@sveltejs/kit' { */ name?: string; /** - * The interval in milliseconds to poll for version changes. If this is `0`, no polling occurs. - * @default 0 + * The interval in milliseconds to poll for version changes. If this is `0`, no polling occurs. SvelteKit also checks for new versions on server responses (via the `x-sveltekit-version` header) and when the tab regains focus or becomes visible, so polling is only needed for long-lived sessions on a single page. + * @default 3600000 */ pollInterval?: number; }; @@ -3619,7 +3619,7 @@ declare module '$app/state' { complete: null; }; /** - * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. + * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on every server response (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. * */ export const updated: { get current(): boolean; From f0554ed29cfd62d0878316c4c527c4a09fdba970 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 23 Jul 2026 17:16:45 -0600 Subject: [PATCH 2/5] lint --- packages/kit/src/runtime/client/state.svelte.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/state.svelte.spec.js b/packages/kit/src/runtime/client/state.svelte.spec.js index f16728d5cb20..6ecd84a76557 100644 --- a/packages/kit/src/runtime/client/state.svelte.spec.js +++ b/packages/kit/src/runtime/client/state.svelte.spec.js @@ -86,7 +86,7 @@ describe('updated', () => { test('check() does not run concurrent checks', async () => { /** @type {Array<{ resolve: (value: any) => void }>} */ - let resolve_queue = []; + const resolve_queue = []; vi.stubGlobal( 'fetch', From 7e45449ea9e0035eb069f0aedd942efeac32ba58 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 23 Jul 2026 18:11:42 -0600 Subject: [PATCH 3/5] conditionally --- packages/kit/src/runtime/server/respond.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 555496d37809..73c4883fe13f 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -521,8 +521,16 @@ export async function internal_respond(request, options, manifest, state) { response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); } + // inform the client of the current app version, so it can detect + // deployments without polling. Skipped during prerendering, since + // prerendered responses are static and the header would be stale. if (!state.prerendering && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { - response.headers.set('x-sveltekit-version', options.version); + try { + response.headers.set('x-sveltekit-version', options.version); + } catch { + // the response might have immutable headers (e.g. if it was + // returned directly from event.fetch) — skip the header in that case + } } resolve_span.setAttributes({ From 026e0a8f12c47cf0f6c9de12dc751e6b6e606a54 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 24 Jul 2026 09:39:45 -0600 Subject: [PATCH 4/5] Update packages/kit/src/runtime/client/state.svelte.js Co-authored-by: Rich Harris --- packages/kit/src/runtime/client/state.svelte.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/state.svelte.js b/packages/kit/src/runtime/client/state.svelte.js index f310960f2a21..4f309d0eebc8 100644 --- a/packages/kit/src/runtime/client/state.svelte.js +++ b/packages/kit/src/runtime/client/state.svelte.js @@ -47,7 +47,9 @@ if (!DEV && BROWSER && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { * the app was hydrated with. Called from the server response header path. * Does NOT reset the poll timer — unlike `check()`, this is a passive observation * from a single server instance's response, not an explicit version check. The - * poll timer continues on its original schedule as a backstop. + * poll timer continues on its original schedule as a backstop. This is important + * for platforms that implement skew protection, where `x-sveltekit-version` + * may be out of date — in this case we still need to poll for `version.json` * @param {string | null} new_version */ notify_version = (new_version) => { From 45649575dbe4ad35e6b19ab698c59fe071dfe8a7 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 24 Jul 2026 12:06:38 -0600 Subject: [PATCH 5/5] only send on stuff that's handled clientside --- .changeset/more-frequent-updated-checks.md | 2 +- packages/kit/src/exports/public.d.ts | 2 +- packages/kit/src/runtime/app/state/index.js | 2 +- .../kit/src/runtime/client/state.svelte.js | 88 ++++++++++--------- .../src/runtime/client/state.svelte.spec.js | 62 ++++++++++++- packages/kit/src/runtime/server/data/index.js | 59 +++++++------ .../kit/src/runtime/server/page/actions.js | 6 +- .../src/runtime/server/remote-functions.js | 6 +- packages/kit/src/runtime/server/respond.js | 16 ---- packages/kit/src/runtime/server/utils.js | 10 +++ .../kit/test/apps/basics/test/server.test.js | 5 ++ packages/kit/test/apps/options-3/test/test.js | 6 +- packages/kit/types/index.d.ts | 4 +- 13 files changed, 167 insertions(+), 101 deletions(-) diff --git a/.changeset/more-frequent-updated-checks.md b/.changeset/more-frequent-updated-checks.md index 667240c7920e..b14549acc8db 100644 --- a/.changeset/more-frequent-updated-checks.md +++ b/.changeset/more-frequent-updated-checks.md @@ -2,4 +2,4 @@ '@sveltejs/kit': breaking --- -breaking: detect new deployments on every server response, tab focus, and visibility change, and default `version.pollInterval` to 1 hour +breaking: detect new deployments on data, remote, and form action responses, tab focus, and visibility change, and default `version.pollInterval` to 1 hour diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 5c7ffe5b9c0a..d32cbfa08a3c 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -875,7 +875,7 @@ export interface KitConfig { }; /** * Client-side navigation can be buggy if you deploy a new version of your app while people are using it. If the code for the new page is already loaded, it may have stale content; if it isn't, the app's route manifest may point to a JavaScript file that no longer exists. - * SvelteKit helps you solve this problem through version management. The current version is included in every server response via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. + * SvelteKit helps you solve this problem through version management. The current version is included in data, remote, and form action responses via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. * If SvelteKit encounters an error while loading the page and detects that a new version has been deployed (using the `name` specified here, which defaults to a timestamp of the build) it will fall back to traditional full-page navigation. * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use `beforeNavigate`: * ```html diff --git a/packages/kit/src/runtime/app/state/index.js b/packages/kit/src/runtime/app/state/index.js index 249029bab7c1..e1114315c7d7 100644 --- a/packages/kit/src/runtime/app/state/index.js +++ b/packages/kit/src/runtime/app/state/index.js @@ -58,7 +58,7 @@ export const page = BROWSER ? client_page : server_page; export const navigating = BROWSER ? client_navigating : server_navigating; /** - * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on every server response (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. + * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on data, remote, and form action responses (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. * @type {{ get current(): boolean; check(): Promise; }} */ export const updated = BROWSER ? client_updated : server_updated; diff --git a/packages/kit/src/runtime/client/state.svelte.js b/packages/kit/src/runtime/client/state.svelte.js index 4f309d0eebc8..49910754fe23 100644 --- a/packages/kit/src/runtime/client/state.svelte.js +++ b/packages/kit/src/runtime/client/state.svelte.js @@ -33,64 +33,66 @@ export const updated = new (class Updated { */ export let notify_version = () => {}; -if (!DEV && BROWSER && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { +if (!DEV && BROWSER) { const interval = __SVELTEKIT_APP_VERSION_POLL_INTERVAL__; /** @type {number | undefined} */ let timeout; - /** True while a version check request is in-flight, to prevent concurrent checks */ - let checking = false; - - /** - * Mark `updated.current` as `true` if the given version differs from the one - * the app was hydrated with. Called from the server response header path. - * Does NOT reset the poll timer — unlike `check()`, this is a passive observation - * from a single server instance's response, not an explicit version check. The - * poll timer continues on its original schedule as a backstop. This is important - * for platforms that implement skew protection, where `x-sveltekit-version` - * may be out of date — in this case we still need to poll for `version.json` - * @param {string | null} new_version - */ - notify_version = (new_version) => { - if (new_version && new_version !== version) { - updated.current = true; - } - }; + /** @type {Promise | undefined} */ + let checking; + + if (__SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { + /** + * Mark `updated.current` as `true` if the given version differs from the one + * the app was hydrated with. Called from the server response header path. + * Does NOT reset the poll timer — unlike `check()`, this is a passive observation + * from a single server instance's response, not an explicit version check. The + * poll timer continues on its original schedule as a backstop. This is important + * for platforms that implement skew protection, where `x-sveltekit-version` + * may be out of date — in this case we still need to poll for `version.json`. + * @param {string | null} new_version + */ + notify_version = (new_version) => { + if (new_version && new_version !== version) { + updated.current = true; + } + }; + } /** @type {() => Promise} */ - async function check() { - if (checking) return updated.current; - checking = true; + function check() { + if (checking) return checking; window.clearTimeout(timeout); - if (interval) timeout = window.setTimeout(check, interval); - try { - const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, { - headers: { - 'cache-control': 'no-cache' + return (checking = (async () => { + try { + const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, { + headers: { + 'cache-control': 'no-cache' + } + }); + + if (!res.ok) { + return false; } - }); - if (!res.ok) { - return false; - } + const data = await res.json(); + const new_update = data.version !== version; - const data = await res.json(); - const new_update = data.version !== version; + if (new_update) { + updated.current = true; + } - if (new_update) { - updated.current = true; - window.clearTimeout(timeout); + return new_update; + } catch { + return false; + } finally { + checking = undefined; + if (interval && !updated.current) timeout = window.setTimeout(check, interval); } - - return new_update; - } catch { - return false; - } finally { - checking = false; - } + })()); } if (interval) timeout = window.setTimeout(check, interval); diff --git a/packages/kit/src/runtime/client/state.svelte.spec.js b/packages/kit/src/runtime/client/state.svelte.spec.js index 6ecd84a76557..ad74dec588d0 100644 --- a/packages/kit/src/runtime/client/state.svelte.spec.js +++ b/packages/kit/src/runtime/client/state.svelte.spec.js @@ -1,4 +1,4 @@ -import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; import { updated, notify_version } from './state.svelte.js'; // Mock `esm-env` so the version-check logic is initialised. In the test env, @@ -20,6 +20,12 @@ describe('updated', () => { updated.current = false; }); + afterEach(() => { + vi.useRealTimers(); + vi.stubGlobal('__SVELTEKIT_APP_VERSION_CHECKS_ENABLED__', true); + vi.stubGlobal('__SVELTEKIT_APP_VERSION_POLL_INTERVAL__', 0); + }); + test('notify_version is a no-op when the version matches', () => { // `version` is mocked as '' in the test env notify_version(''); @@ -41,6 +47,21 @@ describe('updated', () => { expect(updated.current).toBe(false); }); + test('check() remains available when response header checks are disabled', async () => { + vi.stubGlobal('__SVELTEKIT_APP_VERSION_CHECKS_ENABLED__', false); + vi.resetModules(); + vi.stubGlobal('fetch', () => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ version: '' }) + }) + ); + + const { updated } = await import('./state.svelte.js'); + expect(await updated.check()).toBe(true); + expect(updated.current).toBe(true); + }); + test('check() fetches version.json and flips current on mismatch', async () => { vi.stubGlobal('fetch', () => Promise.resolve({ @@ -84,6 +105,45 @@ describe('updated', () => { expect(updated.current).toBe(false); }); + test('check() continues polling after a check exceeds the interval', async () => { + vi.useFakeTimers(); + vi.stubGlobal('__SVELTEKIT_APP_VERSION_POLL_INTERVAL__', 10); + vi.resetModules(); + + /** @type {Array<(value: Response) => void>} */ + const resolve_queue = []; + vi.stubGlobal( + 'fetch', + () => + new Promise((resolve) => { + resolve_queue.push(resolve); + }) + ); + + const { updated } = await import('./state.svelte.js'); + const first = updated.check(); + expect(resolve_queue).toHaveLength(1); + + await vi.advanceTimersByTimeAsync(20); + expect(resolve_queue).toHaveLength(1); + + resolve_queue[0]( + new Response(JSON.stringify({ version: '' }), { + headers: { 'content-type': 'application/json' } + }) + ); + await first; + + await vi.advanceTimersByTimeAsync(10); + expect(resolve_queue).toHaveLength(2); + + resolve_queue[1]( + new Response(JSON.stringify({ version: '' }), { + headers: { 'content-type': 'application/json' } + }) + ); + }); + test('check() does not run concurrent checks', async () => { /** @type {Array<{ resolve: (value: any) => void }>} */ const resolve_queue = []; diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 93bdde16a6f1..e65fbe5c0230 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -7,6 +7,7 @@ import { load_server_data } from '../page/load_data.js'; import { handle_error_and_jsonify } from '../errors.js'; import { normalize_path } from '../../../utils/url.js'; import { text_encoder } from '../../utils.js'; +import { with_version_header } from '../utils.js'; /** * @param {import('@sveltejs/kit').RequestEvent} event @@ -31,9 +32,7 @@ export async function render_data( ) { if (!route.page) { // requesting /__data.json should fail for a +server.js - return new Response(undefined, { - status: 404 - }); + return with_version_header(new Response(undefined, { status: 404 })); } try { @@ -123,26 +122,28 @@ export async function render_data( return json_response(data); } - return new Response( - new ReadableStream({ - async start(controller) { - controller.enqueue(text_encoder.encode(data)); - for await (const chunk of chunks) { - controller.enqueue(text_encoder.encode(chunk)); + return with_version_header( + new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue(text_encoder.encode(data)); + for await (const chunk of chunks) { + controller.enqueue(text_encoder.encode(chunk)); + } + controller.close(); + }, + + type: 'bytes' + }), + { + headers: { + // we use a proprietary content type to prevent buffering. + // the `text` prefix makes it inspectable + 'content-type': 'text/sveltekit-data', + 'cache-control': 'private, no-store' } - controller.close(); - }, - - type: 'bytes' - }), - { - headers: { - // we use a proprietary content type to prevent buffering. - // the `text` prefix makes it inspectable - 'content-type': 'text/sveltekit-data', - 'cache-control': 'private, no-store' } - } + ) ); } catch (e) { const error = normalize_error(e); @@ -161,13 +162,15 @@ export async function render_data( * @param {number} [status] */ function json_response(json, status = 200) { - return text(typeof json === 'string' ? json : JSON.stringify(json), { - status, - headers: { - 'content-type': 'application/json', - 'cache-control': 'private, no-store' - } - }); + return with_version_header( + text(typeof json === 'string' ? json : JSON.stringify(json), { + status, + headers: { + 'content-type': 'application/json', + 'cache-control': 'private, no-store' + } + }) + ); } /** diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index 2418649b5bda..315fff550560 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -7,7 +7,7 @@ import { HttpError, Redirect, ActionFailure, SvelteKitError } from '@sveltejs/ki import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server'; import { normalize_error } from '../../../utils/error.js'; import { is_form_content_type, negotiate } from '../../../utils/http.js'; -import { create_replacer } from '../utils.js'; +import { create_replacer, with_version_header } from '../utils.js'; import { handle_error_and_jsonify } from '../errors.js'; import { record_span } from '../../telemetry/record_span.js'; @@ -95,7 +95,7 @@ export async function handle_action_json_request(event, event_state, options, se }); } else { // no data returned — use 204 No Content (without a body, per the spec) - return new Response(null, { status: 204 }); + return with_version_header(new Response(null, { status: 204 })); } } catch (e) { const err = normalize_error(e); @@ -148,7 +148,7 @@ export function action_json_redirect(redirect) { * @param {ResponseInit} [init] */ function action_json(data, init) { - return json(data, init); + return with_version_header(json(data, init)); } /** diff --git a/packages/kit/src/runtime/server/remote-functions.js b/packages/kit/src/runtime/server/remote-functions.js index 195c057c4263..08e0f988aaa5 100644 --- a/packages/kit/src/runtime/server/remote-functions.js +++ b/packages/kit/src/runtime/server/remote-functions.js @@ -13,6 +13,7 @@ import { check_incorrect_fail_use } from './page/actions.js'; import { DEV } from 'esm-env'; import { record_span } from '../telemetry/record_span.js'; import { deserialize_binary_form } from '../form-utils.js'; +import { with_version_header } from './utils.js'; /** * How long (in milliseconds) to wait after the last message was sent before @@ -28,11 +29,12 @@ export async function handle_remote_call(event, state, options, manifest, id) { attributes: { 'sveltekit.remote.call.id': id }, - fn: (current) => { + fn: async (current) => { const traced_event = merge_tracing(event, current); - return with_request_store({ event: traced_event, state }, () => + const response = await with_request_store({ event: traced_event, state }, () => handle_remote_call_internal(traced_event, state, options, manifest, id) ); + return with_version_header(response); } }); } diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 73c4883fe13f..be9b86052bec 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -521,18 +521,6 @@ export async function internal_respond(request, options, manifest, state) { response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); } - // inform the client of the current app version, so it can detect - // deployments without polling. Skipped during prerendering, since - // prerendered responses are static and the header would be stale. - if (!state.prerendering && __SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { - try { - response.headers.set('x-sveltekit-version', options.version); - } catch { - // the response might have immutable headers (e.g. if it was - // returned directly from event.fetch) — skip the header in that case - } - } - resolve_span.setAttributes({ 'http.response.status_code': response.status, 'http.response.body.size': @@ -571,10 +559,6 @@ export async function internal_respond(request, options, manifest, state) { if (value) headers.set(key, value); } - // preserve the version header so a 304'd data request still tells the client the current version - const version = response.headers.get('x-sveltekit-version'); - if (version) headers.set('x-sveltekit-version', version); - for (const cookie of response.headers.getSetCookie()) { headers.append('set-cookie', cookie); } diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 675b40305ec7..75351e22bd59 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -50,6 +50,16 @@ export function redirect_response(status, location) { return response; } +/** + * @param {Response} response + */ +export function with_version_header(response) { + if (__SVELTEKIT_APP_VERSION_CHECKS_ENABLED__) { + response.headers.set('x-sveltekit-version', __SVELTEKIT_APP_VERSION__); + } + return response; +} + /** * @param {import('@sveltejs/kit').RequestEvent} event * @param {Error & { path: string }} error diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 2ec11b8ab827..bf2607ee2173 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -969,6 +969,11 @@ test.describe('Miscellaneous', () => { expect(response.status()).toBe(200); }); + test('does not send x-sveltekit-version header on document responses', async ({ page }) => { + const response = await page.goto('/'); + expect(response?.headers()['x-sveltekit-version']).toBeUndefined(); + }); + test('sends x-sveltekit-version header on data responses', async ({ request }) => { const response = await request.get('/__data.json'); expect(response.headers()['x-sveltekit-version']).toBeTruthy(); diff --git a/packages/kit/test/apps/options-3/test/test.js b/packages/kit/test/apps/options-3/test/test.js index fe9f8a9435f0..91ba50c75f97 100644 --- a/packages/kit/test/apps/options-3/test/test.js +++ b/packages/kit/test/apps/options-3/test/test.js @@ -33,8 +33,8 @@ test.describe("bundleStrategy: 'inline'", () => { expect(fs.existsSync(`${client}/_app/version.json`)).toBe(true); }); - test('does not send x-sveltekit-version header when checks are disabled', async ({ page }) => { - const response = await page.goto('/serialization-stream'); - expect(response?.headers()['x-sveltekit-version']).toBeUndefined(); + test('does not send x-sveltekit-version header when checks are disabled', async ({ request }) => { + const response = await request.get('/serialization-stream/__data.json'); + expect(response.headers()['x-sveltekit-version']).toBeUndefined(); }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index f8750c0743dd..1f13ead141dc 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -847,7 +847,7 @@ declare module '@sveltejs/kit' { }; /** * Client-side navigation can be buggy if you deploy a new version of your app while people are using it. If the code for the new page is already loaded, it may have stale content; if it isn't, the app's route manifest may point to a JavaScript file that no longer exists. - * SvelteKit helps you solve this problem through version management. The current version is included in every server response via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. + * SvelteKit helps you solve this problem through version management. The current version is included in data, remote, and form action responses via the `x-sveltekit-version` header, so SvelteKit can detect new deployments without polling — for example when a navigation triggers a server `load` function, or when a remote function is called. SvelteKit also checks for new versions when the tab regains focus or becomes visible. * If SvelteKit encounters an error while loading the page and detects that a new version has been deployed (using the `name` specified here, which defaults to a timestamp of the build) it will fall back to traditional full-page navigation. * Not all navigations will result in an error though, for example if the JavaScript for the next page is already loaded. If you still want to force a full-page navigation in these cases, use `beforeNavigate`: * ```html @@ -3619,7 +3619,7 @@ declare module '$app/state' { complete: null; }; /** - * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on every server response (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. + * A read-only reactive value that's initially `false`. SvelteKit checks for new versions on data, remote, and form action responses (via the `x-sveltekit-version` header), when the tab regains focus or becomes visible, and on a poll interval (see [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version)). `updated.current` is set to `true` when a new version is detected. `updated.check()` will force an immediate check, regardless of polling. * */ export const updated: { get current(): boolean;