Skip to content
Source
Sign inSign up
Open History to see the last commit

versionSkew.ts

166 lines5.5 KB

1import type {
2 EnvironmentId,
3 ServerConfig,
4 ServerInstallation,
5 ServerSelfUpdateCapability,
6} from "@t3tools/contracts";
7import type { ServerUpdateState } from "@t3tools/client-runtime/state/server";
8import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver";
9import * as Schema from "effect/Schema";
10​
11import { APP_VERSION } from "./branding";
12import { getLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage";
13​
14export interface VersionMismatch {
15 readonly clientVersion: string;
16 readonly serverVersion: string;
17 readonly hint: string;
19​
20const VERSION_MISMATCH_DISMISSALS_STORAGE_KEY = "t3code:version-mismatch-dismissals:v1";
21​
22const dismissedServerUpdateFailures = new WeakSet<ServerUpdateState>();
23​
24export function isServerUpdateFailureDismissed(state: ServerUpdateState): boolean {
25 return state.status === "failed" && dismissedServerUpdateFailures.has(state);
27​
28export function dismissServerUpdateFailure(state: ServerUpdateState): void {
29 if (state.status === "failed") dismissedServerUpdateFailures.add(state);
31​
32const VersionMismatchDismissalsSchema = Schema.Struct({
33 keys: Schema.Array(Schema.String),
34});
35​
36type VersionMismatchDismissals = typeof VersionMismatchDismissalsSchema.Type;
37​
38function normalizeVersion(version: string | null | undefined): string | null {
39 const trimmed = version?.trim();
40 return trimmed && trimmed.length > 0 ? trimmed : null;
42​
43function versionCore(version: string): string {
44 return version.replace(/[-+].*$/, "");
46​
47export function resolveVersionMismatch(
48 serverVersion: string | null | undefined,
49 clientVersion: string = APP_VERSION,
50): VersionMismatch | null {
51 const normalizedClientVersion = normalizeVersion(clientVersion);
52 const normalizedServerVersion = normalizeVersion(serverVersion);
53 if (!normalizedClientVersion || !normalizedServerVersion) {
54 return null;
55 }
56​
57 const clientCore = versionCore(normalizedClientVersion);
58 const serverCore = versionCore(normalizedServerVersion);
59 const compareNightlyBuilds =
60 parseSemver(normalizedClientVersion)?.prerelease[0] === "nightly" &&
61 parseSemver(normalizedServerVersion)?.prerelease[0] === "nightly";
62 const serverIsBehind =
63 parseSemver(clientCore) && parseSemver(serverCore)
64 ? compareSemverVersions(
65 compareNightlyBuilds ? normalizedServerVersion : serverCore,
66 compareNightlyBuilds ? normalizedClientVersion : clientCore,
67 ) < 0
68 : normalizedServerVersion !== normalizedClientVersion;
69 if (!serverIsBehind) {
70 return null;
71 }
72​
73 return {
74 clientVersion: normalizedClientVersion,
75 serverVersion: normalizedServerVersion,
76 hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.",
77 };
79​
80export function resolveServerConfigVersionMismatch(
81 serverConfig: Pick<ServerConfig, "environment"> | null | undefined,
82): VersionMismatch | null {
83 return resolveVersionMismatch(serverConfig?.environment.serverVersion);
85​
86export function resolveServerSelfUpdateCapability(
87 serverConfig: Pick<ServerConfig, "environment"> | null | undefined,
88): ServerSelfUpdateCapability | null {
89 return serverConfig?.environment.capabilities.serverSelfUpdate ?? null;
91​
92export function supportsDesktopAppUpdate(
93 serverConfig: Pick<ServerConfig, "environment"> | null | undefined,
94): boolean {
95 return serverConfig?.environment.capabilities.desktopAppUpdate === true;
97​
98export function manualServerUpdateCommand(
99 targetVersion: string,
100 installation?: ServerInstallation,
101): string {
102 const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
103 if (installation?.kind === "npm-global") {
104 return `npm install --global --prefix ${quote(installation.prefix)} t3@${targetVersion}`;
105 }
106 if (installation?.kind === "t3-launcher") {
107 // An inherited T3CODE_HOME would otherwise send the update to a home the launcher is not in.
108 return `${quote(installation.launcher)} update --base-dir ${quote(installation.baseDir)} ${targetVersion}`;
109 }
110 const runner =
111 installation?.kind === "pnpm-dlx" ? "pnpm dlx" : installation?.kind === "bunx" ? "bunx" : "npx";
112 return `${runner} t3@${targetVersion}`;
114​
115export function buildVersionMismatchDismissalKey(
116 environmentId: EnvironmentId,
117 mismatch: Pick<VersionMismatch, "clientVersion" | "serverVersion">,
118): string {
119 return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`;
121​
122function readVersionMismatchDismissals(): VersionMismatchDismissals {
123 try {
124 return (
125 getLocalStorageItem(
126 VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,
127 VersionMismatchDismissalsSchema,
128 ) ?? { keys: [] }
129 );
130 } catch (error) {
131 console.error("Could not read version-mismatch dismissals.", error);
132 return { keys: [] };
133 }
135​
136function writeVersionMismatchDismissals(document: VersionMismatchDismissals): void {
137 try {
138 setLocalStorageItem(
139 VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,
140 document,
141 VersionMismatchDismissalsSchema,
142 );
143 } catch (error) {
144 console.error("Could not persist version-mismatch dismissals.", error);
145 }
147​
148export function isVersionMismatchDismissed(dismissalKey: string | null | undefined): boolean {
149 if (!dismissalKey) {
150 return false;
151 }
152 return readVersionMismatchDismissals().keys.includes(dismissalKey);
154​
155export function dismissVersionMismatch(dismissalKey: string | null | undefined): void {
156 if (!dismissalKey) {
157 return;
158 }
159 const document = readVersionMismatchDismissals();
160 if (document.keys.includes(dismissalKey)) {
161 return;
162 }
163 writeVersionMismatchDismissals({
164 keys: [...document.keys, dismissalKey],
165 });