cache update center status snapshots (#349)

This commit is contained in:
Cita
2026-03-31 20:47:02 +08:00
committed by GitHub
parent d930b87762
commit baa11d7787
9 changed files with 657 additions and 81 deletions
@@ -4,15 +4,15 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
const {
buildUpdateCenterStatusMock,
refreshUpdateCenterStatusCacheMock,
sendNotificationMock,
} = vi.hoisted(() => ({
buildUpdateCenterStatusMock: vi.fn(),
refreshUpdateCenterStatusCacheMock: vi.fn(),
sendNotificationMock: vi.fn(),
}));
vi.mock('./updateCenterStatusService.js', () => ({
buildUpdateCenterStatus: (...args: unknown[]) => buildUpdateCenterStatusMock(...args),
refreshUpdateCenterStatusCache: (...args: unknown[]) => refreshUpdateCenterStatusCacheMock(...args),
}));
vi.mock('./notifyService.js', () => ({
@@ -51,7 +51,7 @@ describe('updateCenterPollingService', () => {
beforeEach(async () => {
vi.useFakeTimers();
buildUpdateCenterStatusMock.mockReset();
refreshUpdateCenterStatusCacheMock.mockReset();
sendNotificationMock.mockReset();
await db.delete(schema.events).run();
@@ -76,17 +76,59 @@ describe('updateCenterPollingService', () => {
});
it('runs immediately, writes one event, and only notifies once for the same candidate', async () => {
buildUpdateCenterStatusMock.mockResolvedValue({
currentVersion: '1.2.3',
githubRelease: {
normalizedVersion: '1.3.0',
displayVersion: '1.3.0',
tagName: 'v1.3.0',
},
dockerHubTag: null,
helper: {
imageTag: '1.2.3',
},
refreshUpdateCenterStatusCacheMock.mockImplementation(async () => {
const previousRuntime = await loadUpdateCenterRuntimeState();
const runtime = {
...previousRuntime,
lastCheckedAt: '2026-03-31 12:00:00',
lastCheckError: null,
lastResolvedSource: 'github-release' as const,
lastResolvedDisplayVersion: '1.3.0',
lastResolvedCandidateKey: 'github-release:v1.3.0',
statusSnapshot: {
githubRelease: {
source: 'github-release' as const,
rawVersion: 'v1.3.0',
normalizedVersion: '1.3.0',
url: 'https://github.com/cita-777/metapi/releases/tag/v1.3.0',
tagName: 'v1.3.0',
digest: null,
displayVersion: '1.3.0',
publishedAt: '2026-03-31T12:00:00Z',
},
dockerHubTag: null,
helper: {
ok: true,
releaseName: 'metapi',
namespace: 'ai',
revision: '12',
imageRepository: '1467078763/metapi',
imageTag: '1.2.3',
imageDigest: null,
healthy: true,
history: [],
},
},
};
return {
status: {
currentVersion: '1.2.3',
githubRelease: runtime.statusSnapshot.githubRelease,
dockerHubTag: null,
helper: runtime.statusSnapshot.helper,
runtime,
},
candidate: {
source: 'github-release',
kind: 'new-version',
candidateKey: 'github-release:v1.3.0',
displayVersion: '1.3.0',
tagName: 'v1.3.0',
digest: null,
},
previousRuntime,
runtime,
};
});
startUpdateCenterPolling(60_000);
@@ -109,6 +151,15 @@ describe('updateCenterPollingService', () => {
lastResolvedSource: 'github-release',
lastResolvedCandidateKey: 'github-release:v1.3.0',
lastNotifiedCandidateKey: 'github-release:v1.3.0',
statusSnapshot: {
githubRelease: expect.objectContaining({
normalizedVersion: '1.3.0',
}),
dockerHubTag: null,
helper: expect.objectContaining({
imageTag: '1.2.3',
}),
},
}));
await vi.advanceTimersByTimeAsync(60_000);
@@ -119,7 +170,7 @@ describe('updateCenterPollingService', () => {
});
it('stores the latest check error without creating events or notifications when the background check fails', async () => {
buildUpdateCenterStatusMock.mockRejectedValue(new Error('GitHub releases lookup timed out'));
refreshUpdateCenterStatusCacheMock.mockRejectedValue(new Error('GitHub releases lookup timed out'));
startUpdateCenterPolling(60_000);
await vi.advanceTimersByTimeAsync(0);
@@ -133,19 +184,60 @@ describe('updateCenterPollingService', () => {
});
it('persists the notified candidate even when the downstream notification send fails', async () => {
buildUpdateCenterStatusMock.mockImplementation(async () => ({
currentVersion: '1.2.3',
githubRelease: {
normalizedVersion: '1.3.0',
displayVersion: '1.3.0',
tagName: 'v1.3.0',
},
dockerHubTag: null,
helper: {
imageTag: '1.2.3',
},
runtime: await loadUpdateCenterRuntimeState(),
}));
refreshUpdateCenterStatusCacheMock.mockImplementation(async () => {
const previousRuntime = await loadUpdateCenterRuntimeState();
const runtime = {
...previousRuntime,
lastCheckedAt: '2026-03-31 12:01:00',
lastCheckError: null,
lastResolvedSource: 'github-release' as const,
lastResolvedDisplayVersion: '1.3.0',
lastResolvedCandidateKey: 'github-release:v1.3.0',
statusSnapshot: {
githubRelease: {
source: 'github-release' as const,
rawVersion: 'v1.3.0',
normalizedVersion: '1.3.0',
url: 'https://github.com/cita-777/metapi/releases/tag/v1.3.0',
tagName: 'v1.3.0',
digest: null,
displayVersion: '1.3.0',
publishedAt: '2026-03-31T12:01:00Z',
},
dockerHubTag: null,
helper: {
ok: true,
releaseName: 'metapi',
namespace: 'ai',
revision: '12',
imageRepository: '1467078763/metapi',
imageTag: '1.2.3',
imageDigest: null,
healthy: true,
history: [],
},
},
};
return {
status: {
currentVersion: '1.2.3',
githubRelease: runtime.statusSnapshot.githubRelease,
dockerHubTag: null,
helper: runtime.statusSnapshot.helper,
runtime,
},
candidate: {
source: 'github-release',
kind: 'new-version',
candidateKey: 'github-release:v1.3.0',
displayVersion: '1.3.0',
tagName: 'v1.3.0',
digest: null,
},
previousRuntime,
runtime,
};
});
sendNotificationMock.mockRejectedValue(new Error('notification downstream failed'));
startUpdateCenterPolling(60_000);
@@ -155,10 +247,19 @@ describe('updateCenterPollingService', () => {
expect(sendNotificationMock).toHaveBeenCalledTimes(1);
expect(await db.select().from(schema.events).all()).toHaveLength(1);
expect(await loadUpdateCenterRuntimeState()).toEqual(expect.objectContaining({
lastCheckError: null,
lastCheckError: 'notification downstream failed',
lastResolvedCandidateKey: 'github-release:v1.3.0',
lastNotifiedCandidateKey: 'github-release:v1.3.0',
lastNotifiedAt: expect.any(String),
statusSnapshot: {
githubRelease: expect.objectContaining({
normalizedVersion: '1.3.0',
}),
dockerHubTag: null,
helper: expect.objectContaining({
imageTag: '1.2.3',
}),
},
}));
});
});
@@ -1,9 +1,9 @@
import { db, schema } from '../db/index.js';
import { formatUtcSqlDateTime } from './localTimeService.js';
import { sendNotification } from './notifyService.js';
import { buildUpdateCenterStatus } from './updateCenterStatusService.js';
import { refreshUpdateCenterStatusCache } from './updateCenterStatusService.js';
import { loadUpdateCenterRuntimeState, saveUpdateCenterRuntimeState } from './updateCenterRuntimeStateService.js';
import { resolveUpdateReminderCandidate } from './updateCenterReminderService.js';
import type { UpdateReminderCandidate } from './updateCenterReminderService.js';
const DEFAULT_UPDATE_CENTER_INTERVAL_MS = 15 * 60 * 1000;
@@ -15,7 +15,7 @@ function summarizeError(error: unknown) {
return String(error || 'unknown error');
}
function buildReminderEvent(candidate: ReturnType<typeof resolveUpdateReminderCandidate>) {
function buildReminderEvent(candidate: UpdateReminderCandidate | null) {
if (!candidate) return null;
const title = candidate.kind === 'new-digest'
? '更新中心发现新 digest'
@@ -35,23 +35,11 @@ async function runSyncOnce() {
const checkedAt = formatUtcSqlDateTime(new Date());
try {
const status = await buildUpdateCenterStatus();
const previousRuntime = status.runtime || await loadUpdateCenterRuntimeState();
const candidate = resolveUpdateReminderCandidate({
currentVersion: status.currentVersion,
helper: status.helper as { imageTag?: string | null; imageDigest?: string | null } | null,
githubRelease: status.githubRelease,
dockerHubTag: status.dockerHubTag,
});
const nextRuntime = {
...previousRuntime,
lastCheckedAt: checkedAt,
lastCheckError: null,
lastResolvedSource: candidate?.source || null,
lastResolvedDisplayVersion: candidate?.displayVersion || null,
lastResolvedCandidateKey: candidate?.candidateKey || null,
};
const {
candidate,
previousRuntime,
runtime,
} = await refreshUpdateCenterStatusCache(checkedAt);
if (candidate && candidate.candidateKey !== previousRuntime.lastNotifiedCandidateKey) {
const reminderEvent = buildReminderEvent(candidate);
@@ -64,16 +52,16 @@ async function runSyncOnce() {
relatedType: 'update_center',
createdAt: checkedAt,
}).run();
nextRuntime.lastNotifiedCandidateKey = candidate.candidateKey;
nextRuntime.lastNotifiedAt = checkedAt;
await saveUpdateCenterRuntimeState(nextRuntime);
await saveUpdateCenterRuntimeState({
...runtime,
lastNotifiedCandidateKey: candidate.candidateKey,
lastNotifiedAt: checkedAt,
});
await sendNotification(reminderEvent.title, reminderEvent.message, 'info', {
bypassThrottle: true,
});
}
}
await saveUpdateCenterRuntimeState(nextRuntime);
} catch (error) {
const previousRuntime = await loadUpdateCenterRuntimeState();
await saveUpdateCenterRuntimeState({
@@ -54,6 +54,7 @@ describe('updateCenterRuntimeStateService', () => {
lastResolvedCandidateKey: null,
lastNotifiedCandidateKey: null,
lastNotifiedAt: null,
statusSnapshot: null,
});
});
@@ -66,6 +67,50 @@ describe('updateCenterRuntimeStateService', () => {
lastResolvedCandidateKey: 'docker-hub-tag:latest@sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
lastNotifiedCandidateKey: 'docker-hub-tag:latest@sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
lastNotifiedAt: '2026-03-30 20:31:00',
statusSnapshot: {
githubRelease: {
source: 'github-release',
rawVersion: 'v1.3.0',
normalizedVersion: '1.3.0',
url: 'https://github.com/cita-777/metapi/releases/tag/v1.3.0',
tagName: 'v1.3.0',
digest: null,
displayVersion: '1.3.0',
publishedAt: '2026-03-30T20:30:00Z',
},
dockerHubTag: {
source: 'docker-hub-tag',
rawVersion: 'latest',
normalizedVersion: 'latest',
url: null,
tagName: 'latest',
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
displayVersion: 'latest @ sha256:efb2ee655386',
publishedAt: '2026-03-30T20:30:00Z',
},
helper: {
ok: true,
releaseName: 'metapi',
namespace: 'ai',
revision: '12',
imageRepository: '1467078763/metapi',
imageTag: 'latest',
imageDigest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
healthy: true,
error: undefined,
history: [
{
revision: '11',
updatedAt: '2026-03-29T20:00:00Z',
status: 'superseded',
description: 'Rollback to stable digest',
imageRepository: '1467078763/metapi',
imageTag: 'main',
imageDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
],
},
},
});
await expect(loadUpdateCenterRuntimeState()).resolves.toEqual({
@@ -76,6 +121,50 @@ describe('updateCenterRuntimeStateService', () => {
lastResolvedCandidateKey: 'docker-hub-tag:latest@sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
lastNotifiedCandidateKey: 'docker-hub-tag:latest@sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
lastNotifiedAt: '2026-03-30 20:31:00',
statusSnapshot: {
githubRelease: {
source: 'github-release',
rawVersion: 'v1.3.0',
normalizedVersion: '1.3.0',
url: 'https://github.com/cita-777/metapi/releases/tag/v1.3.0',
tagName: 'v1.3.0',
digest: null,
displayVersion: '1.3.0',
publishedAt: '2026-03-30T20:30:00Z',
},
dockerHubTag: {
source: 'docker-hub-tag',
rawVersion: 'latest',
normalizedVersion: 'latest',
url: null,
tagName: 'latest',
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
displayVersion: 'latest @ sha256:efb2ee655386',
publishedAt: '2026-03-30T20:30:00Z',
},
helper: {
ok: true,
releaseName: 'metapi',
namespace: 'ai',
revision: '12',
imageRepository: '1467078763/metapi',
imageTag: 'latest',
imageDigest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
healthy: true,
error: undefined,
history: [
{
revision: '11',
updatedAt: '2026-03-29T20:00:00Z',
status: 'superseded',
description: 'Rollback to stable digest',
imageRepository: '1467078763/metapi',
imageTag: 'main',
imageDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
],
},
},
});
});
});
@@ -2,7 +2,14 @@ import { eq } from 'drizzle-orm';
import { db, schema } from '../db/index.js';
import { upsertSetting } from '../db/upsertSetting.js';
import type { UpdateCenterVersionSource } from './updateCenterVersionService.js';
import type { UpdateCenterHelperStatus } from './updateCenterHelperClient.js';
import type { UpdateCenterVersionCandidate, UpdateCenterVersionSource } from './updateCenterVersionService.js';
export type UpdateCenterStatusSnapshot = {
githubRelease: UpdateCenterVersionCandidate | null;
dockerHubTag: UpdateCenterVersionCandidate | null;
helper: UpdateCenterHelperStatus | null;
};
export type UpdateCenterRuntimeState = {
lastCheckedAt: string | null;
@@ -12,6 +19,7 @@ export type UpdateCenterRuntimeState = {
lastResolvedCandidateKey: string | null;
lastNotifiedCandidateKey: string | null;
lastNotifiedAt: string | null;
statusSnapshot: UpdateCenterStatusSnapshot | null;
};
export const UPDATE_CENTER_RUNTIME_STATE_SETTING_KEY = 'update_center_runtime_state_v1';
@@ -25,6 +33,7 @@ export function getDefaultUpdateCenterRuntimeState(): UpdateCenterRuntimeState {
lastResolvedCandidateKey: null,
lastNotifiedCandidateKey: null,
lastNotifiedAt: null,
statusSnapshot: null,
};
}
@@ -38,6 +47,72 @@ function normalizeNullableSource(value: unknown): UpdateCenterVersionSource | nu
return value === 'docker-hub-tag' || value === 'github-release' ? value : null;
}
function normalizeVersionCandidate(input: unknown): UpdateCenterVersionCandidate | null {
if (!input || typeof input !== 'object') return null;
const record = input as Record<string, unknown>;
const source = normalizeNullableSource(record.source);
const rawVersion = normalizeNullableString(record.rawVersion);
const normalizedVersion = normalizeNullableString(record.normalizedVersion);
if (!source || !rawVersion || !normalizedVersion) return null;
return {
source,
rawVersion,
normalizedVersion,
url: normalizeNullableString(record.url),
tagName: normalizeNullableString(record.tagName),
digest: normalizeNullableString(record.digest),
displayVersion: normalizeNullableString(record.displayVersion),
publishedAt: normalizeNullableString(record.publishedAt),
};
}
function normalizeHelperHistoryEntry(input: unknown): NonNullable<UpdateCenterHelperStatus['history']>[number] | null {
if (!input || typeof input !== 'object') return null;
const record = input as Record<string, unknown>;
const revision = normalizeNullableString(record.revision);
if (!revision) return null;
return {
revision,
updatedAt: normalizeNullableString(record.updatedAt),
status: normalizeNullableString(record.status),
description: normalizeNullableString(record.description),
imageRepository: normalizeNullableString(record.imageRepository),
imageTag: normalizeNullableString(record.imageTag),
imageDigest: normalizeNullableString(record.imageDigest),
};
}
function normalizeHelperSnapshot(input: unknown): UpdateCenterHelperStatus | null {
if (!input || typeof input !== 'object') return null;
const record = input as Record<string, unknown>;
return {
ok: typeof record.ok === 'boolean' ? record.ok : false,
releaseName: normalizeNullableString(record.releaseName),
namespace: normalizeNullableString(record.namespace),
revision: normalizeNullableString(record.revision),
imageRepository: normalizeNullableString(record.imageRepository),
imageTag: normalizeNullableString(record.imageTag),
imageDigest: normalizeNullableString(record.imageDigest),
healthy: typeof record.healthy === 'boolean' ? record.healthy : false,
error: normalizeNullableString(record.error) || undefined,
history: Array.isArray(record.history)
? record.history
.map((entry) => normalizeHelperHistoryEntry(entry))
.filter((entry): entry is NonNullable<UpdateCenterHelperStatus['history']>[number] => !!entry)
: [],
};
}
function normalizeStatusSnapshot(input: unknown): UpdateCenterStatusSnapshot | null {
if (!input || typeof input !== 'object') return null;
const record = input as Record<string, unknown>;
return {
githubRelease: normalizeVersionCandidate(record.githubRelease),
dockerHubTag: normalizeVersionCandidate(record.dockerHubTag),
helper: normalizeHelperSnapshot(record.helper),
};
}
export function normalizeUpdateCenterRuntimeState(input: unknown): UpdateCenterRuntimeState {
const defaults = getDefaultUpdateCenterRuntimeState();
const record = input && typeof input === 'object' ? input as Record<string, unknown> : {};
@@ -49,6 +124,9 @@ export function normalizeUpdateCenterRuntimeState(input: unknown): UpdateCenterR
lastResolvedCandidateKey: normalizeNullableString(record.lastResolvedCandidateKey) ?? defaults.lastResolvedCandidateKey,
lastNotifiedCandidateKey: normalizeNullableString(record.lastNotifiedCandidateKey) ?? defaults.lastNotifiedCandidateKey,
lastNotifiedAt: normalizeNullableString(record.lastNotifiedAt) ?? defaults.lastNotifiedAt,
statusSnapshot: Object.prototype.hasOwnProperty.call(record, 'statusSnapshot')
? normalizeStatusSnapshot(record.statusSnapshot)
: defaults.statusSnapshot,
};
}
@@ -1,4 +1,5 @@
import { config as runtimeConfig } from '../config.js';
import { formatUtcSqlDateTime } from './localTimeService.js';
import { listBackgroundTasks } from './backgroundTaskService.js';
import {
fetchLatestDockerHubTag,
@@ -7,13 +8,21 @@ import {
type UpdateCenterVersionCandidate,
} from './updateCenterVersionService.js';
import {
type UpdateCenterConfig,
loadUpdateCenterConfig,
} from './updateCenterConfigService.js';
import {
getUpdateCenterHelperStatus,
type UpdateCenterHelperStatus,
} from './updateCenterHelperClient.js';
import { loadUpdateCenterRuntimeState } from './updateCenterRuntimeStateService.js';
import {
loadUpdateCenterRuntimeState,
saveUpdateCenterRuntimeState,
type UpdateCenterRuntimeState,
type UpdateCenterStatusSnapshot,
} from './updateCenterRuntimeStateService.js';
import { UPDATE_CENTER_DEPLOY_TASK_TYPE } from './updateCenterTaskConstants.js';
import { resolveUpdateReminderCandidate, type UpdateReminderCandidate } from './updateCenterReminderService.js';
function getUpdateCenterHelperToken(): string {
return String(
@@ -57,7 +66,85 @@ function getDeployTasks() {
return listBackgroundTasks(50).filter((task) => task.type === UPDATE_CENTER_DEPLOY_TASK_TYPE);
}
export async function buildUpdateCenterStatus() {
export type UpdateCenterStatusResult = {
currentVersion: string;
config: UpdateCenterConfig;
githubRelease: UpdateCenterVersionCandidate | null;
dockerHubTag: UpdateCenterVersionCandidate | null;
helper: UpdateCenterHelperStatus;
runningTask: ReturnType<typeof getDeployTasks>[number] | null;
lastFinishedTask: ReturnType<typeof getDeployTasks>[number] | null;
runtime: UpdateCenterRuntimeState;
};
function buildUnavailableHelperStatus(error: string | null = null): UpdateCenterHelperStatus {
return {
ok: false,
releaseName: null,
namespace: null,
revision: null,
imageRepository: null,
imageTag: null,
imageDigest: null,
healthy: false,
error: error || undefined,
history: [],
};
}
function buildStatusSnapshot(status: Pick<UpdateCenterStatusResult, 'githubRelease' | 'dockerHubTag' | 'helper'>): UpdateCenterStatusSnapshot {
return {
githubRelease: status.githubRelease || null,
dockerHubTag: status.dockerHubTag || null,
helper: status.helper || null,
};
}
function buildNextRuntimeState(
status: Pick<UpdateCenterStatusResult, 'currentVersion' | 'githubRelease' | 'dockerHubTag' | 'helper'>,
previousRuntime: UpdateCenterRuntimeState,
checkedAt: string,
): { candidate: UpdateReminderCandidate | null; nextRuntime: UpdateCenterRuntimeState } {
const candidate = resolveUpdateReminderCandidate({
currentVersion: status.currentVersion,
helper: status.helper,
githubRelease: status.githubRelease,
dockerHubTag: status.dockerHubTag,
});
return {
candidate,
nextRuntime: {
...previousRuntime,
lastCheckedAt: checkedAt,
lastCheckError: null,
lastResolvedSource: candidate?.source || null,
lastResolvedDisplayVersion: candidate?.displayVersion || null,
lastResolvedCandidateKey: candidate?.candidateKey || null,
statusSnapshot: buildStatusSnapshot(status),
},
};
}
function buildResponseFromState(config: UpdateCenterConfig, runtime: UpdateCenterRuntimeState): UpdateCenterStatusResult {
const snapshot = runtime.statusSnapshot;
const tasks = getDeployTasks();
const runningTask = tasks.find((task) => task.status === 'pending' || task.status === 'running') || null;
const lastFinishedTask = tasks.find((task) => task.status === 'succeeded' || task.status === 'failed') || null;
return {
currentVersion: getCurrentRuntimeVersion(),
config,
githubRelease: snapshot?.githubRelease || null,
dockerHubTag: snapshot?.dockerHubTag || null,
helper: snapshot?.helper || buildUnavailableHelperStatus(runtime.lastCheckError),
runningTask,
lastFinishedTask,
runtime,
};
}
export async function buildUpdateCenterStatus(): Promise<UpdateCenterStatusResult> {
const config = await loadUpdateCenterConfig();
const helperToken = getUpdateCenterHelperToken();
@@ -75,11 +162,7 @@ export async function buildUpdateCenterStatus() {
const githubRelease = githubLookup.value as UpdateCenterVersionCandidate | null;
const dockerHubTag = dockerLookup.value as UpdateCenterVersionCandidate | null;
const helper: Record<string, unknown> = helperLookup.value || {
ok: false,
healthy: false,
error: helperLookup.error,
};
const helper = (helperLookup.value as UpdateCenterHelperStatus | null) || buildUnavailableHelperStatus(helperLookup.error);
const tasks = getDeployTasks();
const runningTask = tasks.find((task) => task.status === 'pending' || task.status === 'running') || null;
@@ -96,3 +179,40 @@ export async function buildUpdateCenterStatus() {
runtime,
};
}
export async function buildCachedUpdateCenterStatus(): Promise<UpdateCenterStatusResult> {
const [config, runtime] = await Promise.all([
loadUpdateCenterConfig(),
loadUpdateCenterRuntimeState(),
]);
return buildResponseFromState(config, runtime);
}
export async function refreshUpdateCenterStatusCache(checkedAt = formatUtcSqlDateTime(new Date())): Promise<{
status: UpdateCenterStatusResult;
candidate: UpdateReminderCandidate | null;
previousRuntime: UpdateCenterRuntimeState;
runtime: UpdateCenterRuntimeState;
}> {
const status = await buildUpdateCenterStatus();
const previousRuntime = status.runtime || await loadUpdateCenterRuntimeState();
const { candidate, nextRuntime } = buildNextRuntimeState(status, previousRuntime, checkedAt);
const runtime = await saveUpdateCenterRuntimeState(nextRuntime);
return {
status: {
...status,
runtime,
},
candidate,
previousRuntime,
runtime,
};
}
export async function getUpdateCenterStatus(): Promise<UpdateCenterStatusResult> {
const cached = await buildCachedUpdateCenterStatus();
if (cached.runtime.statusSnapshot) {
return cached;
}
return (await refreshUpdateCenterStatusCache()).status;
}