Simplify server pinging

This commit is contained in:
Andras Schmelczer 2026-09-22 21:50:01 +01:00
commit 2d0b7b2db1
2 changed files with 100 additions and 33 deletions

View file

@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { ServerConfig } from "./server-config";
import { SyncService } from "./sync-service";
import { Settings } from "../persistence/settings";
import { Logger } from "../tracing/logger";
import { SUPPORTED_API_VERSION } from "../consts";
const config = {
supportedApiVersion: SUPPORTED_API_VERSION,
isAuthenticated: true,
mergeableFileExtensions: ["md"],
serverVersion: "test"
};
const make = (fetch: typeof globalThis.fetch) =>
new ServerConfig(
new SyncService(
"device",
new Settings(
new Logger(),
{ remoteUri: "http://test" },
async () => {}
),
fetch,
{ get: () => undefined, save: async () => {} }
)
);
test("configuration shares one validated request and retries failures", async () => {
let calls = 0;
const server = make(async () => {
if (++calls === 1) throw new Error("offline");
return Response.json(config);
});
await assert.rejects(server.getConfig(), /offline/);
const [a, b] = await Promise.all([server.getConfig(), server.getConfig()]);
assert.equal(a, b);
assert.equal(calls, 2);
await server.getConfig();
assert.equal(calls, 2);
});
test("connection checks are independent and validate authentication and version", async () => {
let response = config;
const server = make(async () => Response.json(response));
const cached = await server.getConfig();
response = { ...config, isAuthenticated: false };
assert.equal((await server.checkConnection()).isSuccessful, false);
response = { ...config, supportedApiVersion: SUPPORTED_API_VERSION + 1 };
assert.equal((await server.checkConnection()).isSuccessful, false);
assert.equal(await server.getConfig(), cached);
server.reset();
await assert.rejects(server.getConfig(), /Unsupported API/);
response = config;
assert.deepEqual(await server.getConfig(), config);
});
for (const fail of [false, true]) {
test(`reset fences a late ${fail ? "failed" : "successful"} configuration request`, async () => {
const first = Promise.withResolvers<Response>();
let calls = 0;
const server = make(async () =>
++calls === 1 ? first.promise : Response.json(config)
);
const old = server.getConfig();
server.reset();
const current = await server.getConfig();
if (fail) {
first.reject(new Error("old request"));
await assert.rejects(old, /old request/);
} else {
first.resolve(
Response.json({ ...config, mergeableFileExtensions: ["txt"] })
);
await old;
}
assert.equal(await server.getConfig(), current);
assert.equal(calls, 2);
});
}

View file

@ -4,7 +4,6 @@ import {
ServerVersionMismatchError ServerVersionMismatchError
} from "../errors/errors"; } from "../errors/errors";
import type { SyncService } from "./sync-service"; import type { SyncService } from "./sync-service";
import type { PingResponse } from "./types/PingResponse";
export interface ServerConfigData { export interface ServerConfigData {
mergeableFileExtensions: string[]; mergeableFileExtensions: string[];
@ -13,18 +12,16 @@ export interface ServerConfigData {
} }
export class ServerConfig { export class ServerConfig {
private response: Promise<PingResponse> | undefined; private config: Promise<ServerConfigData> | undefined;
private config: ServerConfigData | undefined;
public constructor(private readonly syncService: SyncService) {} public constructor(private readonly syncService: SyncService) { }
private static validateConfig(config: ServerConfigData): void { private static validateConfig(config: ServerConfigData): void {
if (config.supportedApiVersion !== SUPPORTED_API_VERSION) { if (config.supportedApiVersion !== SUPPORTED_API_VERSION) {
const shouldUpgradeClient = const shouldUpgradeClient =
config.supportedApiVersion > SUPPORTED_API_VERSION; config.supportedApiVersion > SUPPORTED_API_VERSION;
throw new ServerVersionMismatchError( throw new ServerVersionMismatchError(
`Unsupported API version: ${config.supportedApiVersion}. Consider upgrading the ${ `Unsupported API version: ${config.supportedApiVersion}. Consider upgrading the ${shouldUpgradeClient ? "client" : "sync-server"
shouldUpgradeClient ? "client" : "sync-server"
} to ensure compatibility` } to ensure compatibility`
); );
} }
@ -41,29 +38,18 @@ export class ServerConfig {
await this.getConfig(); await this.getConfig();
} }
public async checkConnection(forceUpdate = false): Promise<{ public async checkConnection(): Promise<{
isSuccessful: boolean; isSuccessful: boolean;
message: string; message: string;
}> { }> {
try { try {
let { response } = this; const result = await this.syncService.ping();
if (!response || forceUpdate) {
response = this.response = this.syncService.ping();
}
const result: PingResponse = await response; // it must be defined, otherwise we would have thrown above ServerConfig.validateConfig(result);
this.config = result;
if (result.isAuthenticated) {
return {
isSuccessful: true,
message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated`
};
}
return { return {
isSuccessful: false, isSuccessful: true,
message: `Successfully connected to server (version: ${result.serverVersion}) but failed to authenticate` message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated`
}; };
} catch (e) { } catch (e) {
return { return {
@ -75,22 +61,23 @@ export class ServerConfig {
public async getConfig(): Promise<ServerConfigData> { public async getConfig(): Promise<ServerConfigData> {
if (!this.config) { if (!this.config) {
this.response ??= this.syncService.ping(); const pending = this.syncService
try { .ping()
this.config = await this.response; .then((config) => {
} catch (error) { ServerConfig.validateConfig(config);
this.response = undefined; return config;
throw error; })
} .catch((error: unknown) => {
if (this.config === pending) this.config = undefined;
throw error;
});
this.config = pending;
} }
ServerConfig.validateConfig(this.config);
return this.config; return this.config;
} }
public reset(): void { public reset(): void {
this.response = undefined;
this.config = undefined; this.config = undefined;
} }
} }