All files / src/desktop Updater.ts

100% Statements 63/63
100% Branches 24/24
100% Functions 15/15
100% Lines 53/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99                    2x 2x   2x       310x 60x   310x 310x 310x   310x   310x 14x 13x 13x 13x   2x   13x       310x 4x 4x 4x 4x 4x 7x 7x 2x 2x   3x 3x     310x   10x 9x 9x 9x 9x   2x 1x   7x 4x 3x         310x 63x 2x 2x 3x 3x   2x     310x       3x 3x   6x   4x 4x   2x          
import { relaunch } from '@tauri-apps/plugin-process';
import { check, type Update } from '@tauri-apps/plugin-updater';
import { useCallback, useEffect, useRef, useState } from 'react';
 
export type UpdateState =
  | { status: 'idle' | 'checking' | 'latest' | 'unpublished' }
  | { status: 'available' | 'ready'; version: string }
  | { status: 'downloading'; version: string; progress?: number }
  | { status: 'error'; message: string };
 
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
// The updater's error when the latest GitHub release has no latest.json (an unsigned build).
const NO_MANIFEST = 'Could not fetch a valid release JSON';
 
/** Checks GitHub releases for a newer version; with `auto` on, it also installs it at launch and daily. */
export function useUpdater() {
  const [auto, setAuto] = useState(
    () => localStorage.getItem('autoUpdate') === 'true',
  );
  const [state, setState] = useState<UpdateState>({ status: 'idle' });
  const update = useRef<Update | null>(null);
  const busy = useRef(false);
  // An installed update only applies after a restart, so stop checking until then.
  const installed = useRef(false);
 
  const run = useCallback(async (task: () => Promise<void>) => {
    if (busy.current || installed.current) return;
    busy.current = true;
    try {
      await task();
    } catch (e) {
      setState({ status: 'error', message: String(e) });
    } finally {
      busy.current = false;
    }
  }, []);
 
  const download = useCallback(async (found: Update) => {
    const { version } = found;
    let total = 0;
    let received = 0;
    setState({ status: 'downloading', version });
    await found.downloadAndInstall((e) => {
      if (e.event === 'Started') total = e.data.contentLength ?? 0;
      if (e.event !== 'Progress' || !total) return;
      received += e.data.chunkLength;
      setState({ status: 'downloading', version, progress: received / total });
    });
    installed.current = true;
    setState({ status: 'ready', version });
  }, []);
 
  const checkNow = useCallback(
    (install: boolean) =>
      run(async () => {
        setState({ status: 'checking' });
        localStorage.setItem('lastUpdateCheck', String(Date.now()));
        try {
          update.current = await check();
        } catch (e) {
          if (!String(e).includes(NO_MANIFEST)) throw e;
          return setState({ status: 'unpublished' });
        }
        if (!update.current) return setState({ status: 'latest' });
        if (install) return download(update.current);
        setState({ status: 'available', version: update.current.version });
      }),
    [run, download],
  );
 
  useEffect(() => {
    if (!auto) return;
    void checkNow(true);
    const timer = setInterval(() => {
      const last = Number(localStorage.getItem('lastUpdateCheck'));
      if (Date.now() - last >= DAY) void checkNow(true);
    }, HOUR);
    return () => clearInterval(timer);
  }, [auto, checkNow]);
 
  return {
    state,
    auto,
    changeAuto(on: boolean) {
      setAuto(on);
      localStorage.setItem('autoUpdate', String(on));
    },
    check: () => void checkNow(false),
    install: () =>
      void run(async () => {
        if (update.current) await download(update.current);
      }),
    restart: () => void relaunch(),
  };
}
 
export type Updater = ReturnType<typeof useUpdater>;