Pick up type changes

This commit is contained in:
Andras Schmelczer 2026-09-27 21:40:12 +01:00
commit 202576a03a
4 changed files with 53 additions and 25 deletions

View file

@ -23,5 +23,5 @@ export class PermanentSyncError extends Error {}
export class LocalChangesDuringReconciliation extends Error {} export class LocalChangesDuringReconciliation extends Error {}
/** The server rejected a checkpoint from a discarded/restored history. */ // The server rejected a checkpoint from a discarded/restored history.
export class ServerHistoryChangedError extends Error {} export class ServerHistoryChangedError extends Error {}

View file

@ -4,14 +4,14 @@ 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"; import type { ServerConfigResponse } from "./types/ServerConfigResponse";
export class ServerConfig { export class ServerConfig {
private config?: Promise<PingResponse>; private config?: Promise<ServerConfigResponse>;
public constructor(private readonly syncService: SyncService) {} public constructor(private readonly syncService: SyncService) {}
private static validateConfig(config: PingResponse): void { private static validateConfig(config: ServerConfigResponse): 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;
@ -34,7 +34,7 @@ export class ServerConfig {
message: string; message: string;
}> { }> {
try { try {
const result = await this.syncService.ping(); const result = await this.syncService.getServerConfig();
ServerConfig.validateConfig(result); ServerConfig.validateConfig(result);
@ -50,10 +50,10 @@ export class ServerConfig {
} }
} }
public async getConfig(): Promise<PingResponse> { public async getConfig(): Promise<ServerConfigResponse> {
if (!this.config) { if (!this.config) {
const pending = this.syncService const pending = this.syncService
.ping() .getServerConfig()
.then((config) => { .then((config) => {
ServerConfig.validateConfig(config); ServerConfig.validateConfig(config);
return config; return config;

View file

@ -1,3 +1,4 @@
import { HISTORY_HEADER } from "../consts";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { Settings } from "../persistence/settings"; import { Settings } from "../persistence/settings";
@ -25,7 +26,7 @@ test("paused sync sends no requests, while connection checks still work", async
requests++; requests++;
return Response.json( return Response.json(
{ headEventId: 0, endEventId: 0, events: [] }, { headEventId: 0, endEventId: 0, events: [] },
{ headers: { "X-Vault-Link-History": "0:empty" } } { headers: { [HISTORY_HEADER]: "0:empty" } }
); );
}, },
{ {
@ -35,7 +36,7 @@ test("paused sync sends no requests, while connection checks still work", async
); );
await assert.rejects(service.getEvents(0), SyncResetError); await assert.rejects(service.getEvents(0), SyncResetError);
assert.equal(requests, 0); assert.equal(requests, 0);
await service.ping(); await service.getServerConfig();
assert.equal(requests, 1); assert.equal(requests, 1);
service.resume(); service.resume();
assert.equal((await service.getEvents(0)).headEventId, 0); assert.equal((await service.getEvents(0)).headEventId, 0);
@ -56,7 +57,7 @@ test("pause aborts requests and fences late responses from an injected fetch", a
? response.promise ? response.promise
: Response.json( : Response.json(
{ headEventId: 0, endEventId: 0, events: [] }, { headEventId: 0, endEventId: 0, events: [] },
{ headers: { "X-Vault-Link-History": "0:empty" } } { headers: { [HISTORY_HEADER]: "0:empty" } }
); );
}, },
{ {
@ -75,10 +76,7 @@ test("pause aborts requests and fences late responses from an injected fetch", a
await service.getEvents(0); await service.getEvents(0);
assert.equal(signals[1]?.aborted, false); assert.equal(signals[1]?.aborted, false);
response.resolve( response.resolve(
Response.json( Response.json({}, { headers: { [HISTORY_HEADER]: "99:abandoned" } })
{},
{ headers: { "X-Vault-Link-History": "99:abandoned" } }
)
); );
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(saved, ["0:empty"]); assert.deepEqual(saved, ["0:empty"]);
@ -101,7 +99,7 @@ for (const binary of [false, true]) {
bodyReady.resolve(controller); bodyReady.resolve(controller);
} }
}), }),
{ headers: { "X-Vault-Link-History": "1:head" } } { headers: { [HISTORY_HEADER]: "1:head" } }
), ),
{ {
get: (): undefined => undefined, get: (): undefined => undefined,
@ -139,7 +137,7 @@ test("pause waits for a checkpoint save already in progress", async (): Promise<
Response.json( Response.json(
{}, {},
{ {
headers: { "X-Vault-Link-History": "1:head" } headers: { [HISTORY_HEADER]: "1:head" }
} }
), ),
{ {
@ -174,7 +172,7 @@ test("history saves only new checkpoints, including a changed token at the same
async () => async () =>
Response.json( Response.json(
{}, {},
{ headers: { "X-Vault-Link-History": responseCheckpoint } } { headers: { [HISTORY_HEADER]: responseCheckpoint } }
), ),
{ {
get: (): string | undefined => checkpoint, get: (): string | undefined => checkpoint,
@ -212,7 +210,7 @@ for (const checkpoint of [undefined, ""]) {
headers: headers:
checkpoint === undefined checkpoint === undefined
? {} ? {}
: { "X-Vault-Link-History": checkpoint } : { [HISTORY_HEADER]: checkpoint }
} }
), ),
{ {
@ -229,3 +227,30 @@ for (const checkpoint of [undefined, ""]) {
); );
}); });
} }
test("server configuration uses GET /config even when synchronization is paused", async () => {
const config = {
supportedApiVersion: 4,
serverVersion: "test",
isAuthenticated: true,
mergeableFileExtensions: ["md"]
};
const service = new SyncService(
"device",
settings(),
async (input, init) => {
assert.equal(input, "http://test/vaults/test/config");
assert.equal(init?.method, "GET");
assert.equal(init.body, undefined);
assert.equal(new Headers(init.headers).has(HISTORY_HEADER), false);
return Response.json(config);
},
{
get: (): string => "10:old-history",
save: async (): Promise<void> => {
assert.fail("Config must not update the sync checkpoint");
}
}
);
assert.deepEqual(await service.getServerConfig(), config);
});

View file

@ -1,3 +1,4 @@
import { HISTORY_HEADER, HISTORY_MISMATCH_HEADER } from "../consts";
import type { DocumentId, VaultUpdateId } from "../persistence/database"; import type { DocumentId, VaultUpdateId } from "../persistence/database";
import type { Settings } from "../persistence/settings"; import type { Settings } from "../persistence/settings";
import { abortable } from "../utils/abortable"; import { abortable } from "../utils/abortable";
@ -8,7 +9,7 @@ import type { FileManifestUpdateResponse } from "./types/FileManifestUpdateRespo
import type { PutFileContent } from "./types/PutFileContent"; import type { PutFileContent } from "./types/PutFileContent";
import type { PushFileManifest } from "./types/PushFileManifest"; import type { PushFileManifest } from "./types/PushFileManifest";
import type { VaultSnapshot } from "./types/VaultSnapshot"; import type { VaultSnapshot } from "./types/VaultSnapshot";
import type { PingResponse } from "./types/PingResponse"; import type { ServerConfigResponse } from "./types/ServerConfigResponse";
import { import {
AuthenticationError, AuthenticationError,
PermanentSyncError, PermanentSyncError,
@ -45,8 +46,8 @@ export class SyncService {
await this.history.save(undefined); await this.history.save(undefined);
} }
public async ping(): Promise<PingResponse> { public async getServerConfig(): Promise<ServerConfigResponse> {
return this.request("/ping", { ignoreAborts: true }); return this.request("/config", { ignoreAborts: true });
} }
public async getVaultSnapshot(): Promise<VaultSnapshot> { public async getVaultSnapshot(): Promise<VaultSnapshot> {
@ -93,9 +94,11 @@ export class SyncService {
} }
private async recordHistory(response: Response): Promise<void> { private async recordHistory(response: Response): Promise<void> {
const checkpoint = response.headers.get("x-vault-link-history"); const checkpoint = response.headers.get(HISTORY_HEADER);
if (checkpoint === null || checkpoint === "") { if (checkpoint === null || checkpoint === "") {
throw new PermanentSyncError("Missing server history checkpoint HTTP header"); throw new PermanentSyncError(
"Missing server history checkpoint HTTP header"
);
} }
const previous = this.history.get(); const previous = this.history.get();
@ -143,7 +146,7 @@ export class SyncService {
headers: { headers: {
...(checkpoint === undefined ...(checkpoint === undefined
? {} ? {}
: { "X-Vault-Link-History": checkpoint }), : { [HISTORY_HEADER]: checkpoint }),
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Device-Id": this.deviceId, "Device-Id": this.deviceId,
"Content-Type": "application/json" "Content-Type": "application/json"
@ -152,7 +155,7 @@ export class SyncService {
); );
signal.throwIfAborted(); signal.throwIfAborted();
if (response.headers.get("x-vault-link-history-mismatch") === "1") { if (response.headers.get(HISTORY_MISMATCH_HEADER) === "1") {
throw new ServerHistoryChangedError( throw new ServerHistoryChangedError(
"Server history changed; recovering local work" "Server history changed; recovering local work"
); );