mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-04 17:45:24 +00:00
* feat: AI translations at release, 7 new languages and regional fallback * fix: proper plural forms for count strings and full translations * fix: plural forms for selected, hidden and active counts * fix: synced plugin updates failing on desktop and server-only plugins shown as stopped * fix: restore rounded corners on app icons and favicon * feat: desktop app auto updates with automatic, ask first or never modes * feat: community and custom plugin registries * feat: plugin guide for AI tools in the SDK with a lint check * fix: keep the terminal WebSocket open past nginx's 60s default (#1588) * chore: update changelog * feat: show plugin download counts only and stop CI builds inflating them * feat: hide installed plugins in the browse tab by default * chore: pin bundled plugins to latest releases * chore: update translations * fix: focusing a saved password field and saving no longer erases it * fix: tell desktop users when their 2.8 data needs 2.9 first * fix: point windows update info at the x64 installer * fix: keep desktop client tunnel sockets alive behind proxies * chore: pin bundled plugins to latest releases * fix: cover client tunnel keepalive in the c2s runtime tests * fix: typing after opening a host no longer goes to the host search * fix: accept tab and = separators in SSH config import (#1598) * chore: update changelog * chore: drop plugin versions from core changelog * chore: update translations * chore: lint, format, and bump version to 26.10.1 --------- Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com> Co-authored-by: Iain <Iain1970@protonmail.com>
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
/**
|
|
* Download counts for the plugin store, copied from the registry's
|
|
* stats.json into plugin_install_counts.
|
|
*
|
|
* A count is how often the plugin's .tmxplug release files were downloaded.
|
|
* An instance that updates three times counts three times, and copies bundled
|
|
* into Docker images or the desktop app never count. Treat it as a ranking,
|
|
* never as a user count.
|
|
*/
|
|
|
|
import { pluginLogger } from "../utils/logger.js";
|
|
import {
|
|
OFFICIAL_REGISTRY_ID,
|
|
fetchRegistryStats,
|
|
officialSource,
|
|
type RegistrySource,
|
|
} from "./registry-index.js";
|
|
|
|
async function repository() {
|
|
const { createCurrentPluginInstallCountRepository } =
|
|
await import("../database/repositories/factory.js");
|
|
return createCurrentPluginInstallCountRepository();
|
|
}
|
|
|
|
const STALE_MS = 6 * 60 * 60_000;
|
|
const FORCED_MS = 60_000;
|
|
const WAIT_MS = 3_000;
|
|
|
|
let lastSyncAt = 0;
|
|
let running: Promise<number> | null = null;
|
|
|
|
/**
|
|
* Syncs when the counts are older than the registry timer's interval, or
|
|
* older than a minute with force (the Refresh button). Waits a few seconds
|
|
* at most so a slow registry never holds up the store; a sync that runs
|
|
* longer still finishes in the background.
|
|
*/
|
|
export async function refreshDownloadCounts(force = false): Promise<void> {
|
|
const age = Date.now() - lastSyncAt;
|
|
if (!running && age >= (force ? FORCED_MS : STALE_MS)) {
|
|
running = syncDownloadCounts().finally(() => {
|
|
running = null;
|
|
});
|
|
}
|
|
if (!running) return;
|
|
let timeout: NodeJS.Timeout | undefined;
|
|
await Promise.race([
|
|
running,
|
|
new Promise((resolve) => {
|
|
timeout = setTimeout(resolve, WAIT_MS);
|
|
}),
|
|
]);
|
|
clearTimeout(timeout);
|
|
}
|
|
|
|
/** Test seam. */
|
|
export function resetDownloadCountRefresh(): void {
|
|
lastSyncAt = 0;
|
|
running = null;
|
|
}
|
|
|
|
/** Returns how many rows changed. Never throws; a failed sync keeps the old numbers. */
|
|
export async function syncDownloadCounts(): Promise<number> {
|
|
lastSyncAt = Date.now();
|
|
let changed = 0;
|
|
let sources: RegistrySource[] = [officialSource()];
|
|
try {
|
|
const { listSources } = await import("./registries.js");
|
|
sources = await listSources();
|
|
} catch {
|
|
// Official only when the registry choices cannot be read.
|
|
}
|
|
for (const source of sources) {
|
|
try {
|
|
const stats = await fetchRegistryStats(source);
|
|
if (!stats) continue;
|
|
const entries = [...stats.plugins].map(([pluginId, downloads]) => ({
|
|
pluginId,
|
|
count: downloads,
|
|
}));
|
|
changed += await (await repository()).upsertMany(source.id, entries);
|
|
} catch (error) {
|
|
pluginLogger.warn(
|
|
`Could not sync plugin download counts from ${source.id}: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
{ operation: "plugin_registry" },
|
|
);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
export async function readDownloadCounts(
|
|
registryId: string = OFFICIAL_REGISTRY_ID,
|
|
): Promise<Map<string, number>> {
|
|
try {
|
|
const rows = await (await repository()).listByRegistry(registryId);
|
|
return new Map(rows.map((row) => [row.pluginId, row.count]));
|
|
} catch {
|
|
return new Map();
|
|
}
|
|
}
|