Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

src/update-check.ts

94 lines · 2.8 KB · TypeScript

import type { AppVersion } from './version.ts'
import { emptyVersion, normalizeVersion } from './version.ts'

export const UPDATE_CHECK_MIN_INTERVAL_MS = 3000

export type UpdateCheckResult = {
	updateAvailable: boolean
	kind: 'worker' | 'published' | null
	runningSha: string
	latestSha: string
	latest: AppVersion
	hasWaitingWorker: boolean
}

/**
 * Gate rapid update checks (visibility storms / focus churn).
 *
 * @example
 * import { shouldRunUpdateCheck } from 'kody:@kentcdodds/package-app-kit/update-check'
 * shouldRunUpdateCheck({ visible: true, inFlight: false, lastStartedAt: null, now: Date.now() })
 */
export function shouldRunUpdateCheck(input: {
	visible: boolean
	inFlight: boolean
	lastStartedAt: number | null
	now: number
	minIntervalMs?: number
}): boolean {
	if (!input.visible || input.inFlight) return false
	if (input.lastStartedAt == null) return true
	const gap = input.minIntervalMs ?? UPDATE_CHECK_MIN_INTERVAL_MS
	return input.now - input.lastStartedAt >= gap
}

/**
 * Classify whether a waiting SW or a newer published SHA means an update.
 *
 * @example
 * import { describeAvailableUpdate } from 'kody:@kentcdodds/package-app-kit/update-check'
 * describeAvailableUpdate({ runningSha: 'aaa', latestSha: 'bbb', hasWaitingWorker: false })
 */
export function describeAvailableUpdate(input: {
	runningSha: string
	latestSha: string
	hasWaitingWorker: boolean
}): 'worker' | 'published' | null {
	if (input.hasWaitingWorker) return 'worker'
	if (input.runningSha && input.latestSha && input.runningSha !== input.latestSha) {
		return 'published'
	}
	return null
}

/**
 * Compare the running client version vs recorded/latest publish metadata.
 * Use on visit (toast with refresh) and from About's manual check button.
 *
 * @param input.runningSha - SHA embedded in the current app shell
 * @param input.latest - Recorded version from packageStorage / `/api/version`
 * @param input.hasWaitingWorker - Whether a SW is waiting to activate
 * @returns `{ updateAvailable, kind, ... }` for toast / About UI
 *
 * @example
 * import checkForUpdate from 'kody:@kentcdodds/package-app-kit/update-check'
 * const result = checkForUpdate({
 *   runningSha: 'aaa111',
 *   latest: { sha: 'bbb222', message: 'Ship' },
 *   hasWaitingWorker: false,
 * })
 */
export default function checkForUpdate(input: {
	runningSha?: string
	latest?: Partial<AppVersion> | null
	hasWaitingWorker?: boolean
}): UpdateCheckResult {
	const runningSha = String(input.runningSha ?? '').trim()
	const latest = input.latest ? normalizeVersion(input.latest) : emptyVersion()
	const hasWaitingWorker = Boolean(input.hasWaitingWorker)
	const kind = describeAvailableUpdate({
		runningSha,
		latestSha: latest.sha,
		hasWaitingWorker,
	})
	return {
		updateAvailable: kind != null,
		kind,
		runningSha,
		latestSha: latest.sha,
		latest,
		hasWaitingWorker,
	}
}