LCOV - code coverage report
Current view: top level - dashql-app/src/platform/version - web_version_check.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 96.9 % 97 94
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { useLogger } from '../logger/logger_provider.js';
       4              : import { awaitAndSet, Result } from '../../utils/result.js';
       5              : import { Logger, stringifyError } from '../logger/logger.js';
       6              : import { createTrace } from '../logger/trace_context.js';
       7              : import { DASHQL_LATEST_RELEASE_MANIFEST, DASHQL_STABLE_RELEASE_MANIFEST } from '../../globals.js';
       8              : import { LATEST_RELEASE_MANIFEST_CTX, LATEST_UPDATE_MANIFEST_CTX, INSTALLATION_STATUS_CTX, STABLE_RELEASE_MANIFEST_CTX, STABLE_UPDATE_MANIFEST_CTX, VERSION_CHECK_CTX, VERSION_CHECK_REFRESH_CTX, VersionCheckStatusCode, useVersionCheckRefreshState } from './version_check.js';
       9              : 
      10            1 : const LOG_CTX = "version_check";
      11              : 
      12              : type Props = {
      13              :     children: React.ReactElement;
      14              : };
      15              : 
      16              : /// A release bundle
      17              : export interface ReleaseBundle {
      18              :     url: URL;
      19              :     signature: string | null;
      20              :     name: string;
      21              :     bundle_type: string;
      22              :     targets: string[];
      23              : }
      24              : 
      25              : /// A release manifest
      26              : export interface ReleaseManifest {
      27              :     release_id: string;
      28              :     pub_date: Date;
      29              :     version: string;
      30              :     git_commit_hash: string;
      31              :     git_commit_url: string;
      32              :     update_manifest_url: string;
      33              :     bundles: ReleaseBundle[];
      34              : }
      35              : 
      36            1 : function parseReleaseManifest(raw: any): ReleaseManifest {
      37            1 :     if (raw.pub_date) {
      38            1 :         raw.pub_date = new Date(Date.parse(raw.pub_date));
      39              :     }
      40            1 :     for (const bundle of raw.bundles) {
      41            1 :         bundle.url = new URL(bundle.url);
      42              :     }
      43            1 :     return raw as ReleaseManifest;
      44            1 : }
      45              : 
      46              : /// A release channel
      47              : export type ReleaseChannel = "stable" | "latest";
      48              : 
      49              : /// Detect the release channel of an installed version from its version scheme.
      50              : ///
      51              : /// Releases are versioned as `MAJOR.MINOR.PATCH` for stable builds and
      52              : /// `MAJOR.MINOR.PATCH-dev.N` for prereleases on the latest channel (see `bazel/versioning.bzl` and
      53              : /// `dashql-pack/src/release_version.rs`, which derive the channel from the git iteration).
      54              : /// A prerelease suffix therefore uniquely identifies a build of the latest channel.
      55            1 : export function detectReleaseChannel(version: string): ReleaseChannel {
      56            1 :     return version.includes("-") ? "latest" : "stable";
      57            1 : }
      58              : 
      59              : interface ParsedVersion {
      60              :     major: number;
      61              :     minor: number;
      62              :     patch: number;
      63              :     prerelease: string | null;
      64              : }
      65              : 
      66            1 : function parseVersion(version: string): ParsedVersion {
      67            1 :     const [core, prerelease] = version.split("-", 2);
      68            1 :     const [major, minor, patch] = core.split(".").map(x => parseInt(x, 10) || 0);
      69            1 :     return { major, minor, patch, prerelease: prerelease ?? null };
      70            1 : }
      71              : 
      72              : /// Compare the dot-separated identifiers of two semver prerelease strings (e.g. "dev.5").
      73            1 : function comparePrerelease(a: string, b: string): number {
      74            1 :     const as = a.split(".");
      75            1 :     const bs = b.split(".");
      76            1 :     const n = Math.max(as.length, bs.length);
      77            1 :     for (let i = 0; i < n; ++i) {
      78            1 :         const ai = as[i];
      79            1 :         const bi = bs[i];
      80              :         // A smaller set of prerelease fields has lower precedence
      81            1 :         if (ai === undefined) return -1;
      82            1 :         if (bi === undefined) return 1;
      83            1 :         const aNum = /^\d+$/.test(ai);
      84            1 :         const bNum = /^\d+$/.test(bi);
      85            1 :         if (aNum && bNum) {
      86            1 :             const x = parseInt(ai, 10);
      87            1 :             const y = parseInt(bi, 10);
      88            1 :             if (x !== y) return x < y ? -1 : 1;
      89            1 :         } else if (aNum !== bNum) {
      90              :             // Numeric identifiers always have lower precedence than non-numeric ones
      91            0 :             return aNum ? -1 : 1;
      92            1 :         } else if (ai !== bi) {
      93            0 :             return ai < bi ? -1 : 1;
      94              :         }
      95            1 :     }
      96            0 :     return 0;
      97            1 : }
      98              : 
      99              : /// Compare two release versions following semver precedence.
     100              : /// Returns a negative number if `a < b`, zero if equal, and a positive number if `a > b`.
     101            1 : export function compareReleaseVersions(a: string, b: string): number {
     102            1 :     const pa = parseVersion(a);
     103            1 :     const pb = parseVersion(b);
     104            1 :     if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
     105            1 :     if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
     106            1 :     if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
     107              :     // Equal core version: a build without a prerelease suffix outranks a prerelease of the same core
     108            1 :     if (pa.prerelease == null && pb.prerelease == null) return 0;
     109            1 :     if (pa.prerelease == null) return 1;
     110            1 :     if (pb.prerelease == null) return -1;
     111            1 :     return comparePrerelease(pa.prerelease, pb.prerelease);
     112            1 : }
     113              : 
     114              : /// Whether a release should be offered for installation. Installs only move forward, also across
     115              : /// channels: a newer latest build may have migrated the local storage to a schema an older stable cannot read.
     116            1 : export function canInstallReleaseVersion(targetVersion: string, currentVersion: string): boolean {
     117            1 :     return compareReleaseVersions(targetVersion, currentVersion) > 0;
     118            1 : }
     119              : 
     120              : /// Load the release manifest
     121            1 : export async function loadReleaseManifest(channel: ReleaseChannel, url: URL, logger: Logger): Promise<ReleaseManifest> {
     122            1 :     const traced = logger.withTrace(createTrace());
     123            1 :     const start = performance.now();
     124            1 :     traced.info(`Fetching release manifest`, { "channel": channel }, LOG_CTX);
     125            1 :     try {
     126              :         // Fetch the release manifest
     127            1 :         const manifestRequest = await fetch(url, {cache: "no-store"});
     128            1 :         const manifestRaw = (await manifestRequest.json());
     129            1 :         const manifest = parseReleaseManifest(manifestRaw);
     130              :         // Set release manifest
     131            1 :         const end = performance.now();
     132            1 :         traced.info(`Fetched release manifest`, { "channel": channel, "duration": Math.floor(end - start).toString() }, LOG_CTX);
     133            1 :         return manifest;
     134            1 :     } catch (e: any) {
     135            1 :         const end = performance.now();
     136            1 :         traced.warn(`Failed to fetch release manifest`, { "channel": channel, "duration": Math.floor(end - start).toString(), "error": stringifyError(e) }, LOG_CTX);
     137            1 :         throw e;
     138            1 :     }
     139            1 : }
     140              : 
     141            1 : export const WebVersionCheck: React.FC<Props> = (props: Props) => {
     142            1 :     const logger = useLogger();
     143              : 
     144            1 :     const [stableRelease, setStableRelease] = React.useState<Result<ReleaseManifest> | null>(null);
     145            1 :     const [latestRelease, setLatestRelease] = React.useState<Result<ReleaseManifest> | null>(null);
     146              : 
     147            1 :     const check = React.useCallback(async () => {
     148            1 :         await Promise.all([
     149            1 :             awaitAndSet(loadReleaseManifest("stable", DASHQL_STABLE_RELEASE_MANIFEST, logger), setStableRelease),
     150            1 :             awaitAndSet(loadReleaseManifest("latest", DASHQL_LATEST_RELEASE_MANIFEST, logger), setLatestRelease),
     151            1 :         ]);
     152            1 :     }, [logger]);
     153            1 :     const refresh = useVersionCheckRefreshState(check);
     154              : 
     155            1 :     return (
     156            1 :         <VERSION_CHECK_CTX.Provider value={VersionCheckStatusCode.Disabled}>
     157            1 :             <VERSION_CHECK_REFRESH_CTX.Provider value={refresh}>
     158            1 :                 <INSTALLATION_STATUS_CTX.Provider value={null}>
     159            1 :                     <STABLE_RELEASE_MANIFEST_CTX.Provider value={stableRelease}>
     160            1 :                         <STABLE_UPDATE_MANIFEST_CTX.Provider value={null}>
     161            1 :                             <LATEST_RELEASE_MANIFEST_CTX.Provider value={latestRelease}>
     162            1 :                                 <LATEST_UPDATE_MANIFEST_CTX.Provider value={null}>
     163            1 :                                     {props.children}
     164            1 :                                 </LATEST_UPDATE_MANIFEST_CTX.Provider>
     165            1 :                             </LATEST_RELEASE_MANIFEST_CTX.Provider>
     166            1 :                         </STABLE_UPDATE_MANIFEST_CTX.Provider>
     167            1 :                     </STABLE_RELEASE_MANIFEST_CTX.Provider>
     168            1 :                 </INSTALLATION_STATUS_CTX.Provider>
     169            1 :             </VERSION_CHECK_REFRESH_CTX.Provider>
     170            1 :         </VERSION_CHECK_CTX.Provider>
     171              :     );
     172            1 : };
        

Generated by: LCOV version 2.0-1