Comments & make ping refreshable

This commit is contained in:
Andras Schmelczer 2026-09-30 08:27:24 +01:00
commit b78ab61992
7 changed files with 38 additions and 22 deletions

View file

@ -28,6 +28,11 @@ export class ConflictingPathError extends Error {
export class PermanentSyncError extends Error {}
/**
* Retry reconciliation against fresh local state. Raised for stale identity plans
* and local files that change during history recovery. Syncer schedules another
* pass without reporting this as a sync failure.
*/
export class LocalChangesDuringReconciliation extends Error {}
// The server rejected a checkpoint from a discarded/restored history.

View file

@ -52,7 +52,7 @@ export class FileChangeSession {
return;
}
this.options.checkpoint.assertUnchanged();
this.options.assertUnchanged();
}
public beforeFileMutation(): void {
@ -64,7 +64,9 @@ export class FileChangeSession {
const { localChanges, isProtectedPath } = this.options;
await localChanges.flush();
this.abortIfStale();
if (!this.mutationsStarted) {
this.options.assertUnchanged();
}
for (const change of getUnappliedChanges(
this.planned,

View file

@ -4,7 +4,6 @@ import { test } from "node:test";
import { MemoryDisk } from "../../../test-support/storage";
import { Database, createEmptyDatabase } from "../persistence/database";
import type { ServerConfig } from "../services/server-config";
import { LocalChangeCheckpoint } from "../sync-operations/local-change-checkpoint";
import { hash } from "../utils/hash";
import { FileOperations } from "./file-operations";
@ -27,7 +26,6 @@ test("a path swap uses a conflict path only as a temporary destination", async (
async () => undefined
);
const files = new FileOperations(disk, database, {} as ServerConfig);
const checkpoint = new LocalChangeCheckpoint({ generation: 0 });
const renames: string[] = [];
disk.boundary = (label): void => {
if (label.startsWith("visible:rename:")) {
@ -37,7 +35,7 @@ test("a path swap uses a conflict path only as a temporary destination", async (
const next = structuredClone(database.state);
next.actualFileManifest = { a: "b.md", b: "a.md" };
await files.applyChanges(next, checkpoint);
await files.applyChanges(next, () => undefined);
assert.deepEqual(renames, [
"b.md->b (conflict b).md", // Free B without losing its contents.

View file

@ -12,7 +12,7 @@ export interface ContentHead {
}
export interface DocumentState {
// A null version retains only the clean hash after a server history reset.
// A null version retains an old-history hash until a scan can classify edits.
base?: { vaultUpdateId: VaultUpdateId | null; hash: string };
// Last observed disk hash; an empty string means a notification awaits scanning.

View file

@ -13,9 +13,8 @@ const config = {
mergeableFileExtensions: ["md"],
serverVersion: "test"
};
const make = (fetch: typeof globalThis.fetch): ServerConfig =>
new ServerConfig(
new SyncService(
const make = (fetch: typeof globalThis.fetch): ServerConfig => {
const service = new SyncService(
"device",
new Settings(
new Logger(),
@ -23,9 +22,14 @@ const make = (fetch: typeof globalThis.fetch): ServerConfig =>
async () => undefined
),
fetch,
{ get: () => undefined, save: async () => undefined }
)
{
get: (): undefined => undefined,
save: async (): Promise<void> => undefined
}
);
service.resume();
return new ServerConfig(service);
};
test("configuration shares one validated request and retries failures", async () => {
let calls = 0;

View file

@ -53,7 +53,7 @@ export class ServerConfig {
public async getConfig(): Promise<ServerConfigResponse> {
if (!this.config) {
const pending = this.syncService
.getServerConfig()
.getServerConfig(false)
.then((config) => {
ServerConfig.validateConfig(config);
return config;

View file

@ -46,8 +46,13 @@ export class SyncService {
await this.history.save(undefined);
}
public async getServerConfig(): Promise<ServerConfigResponse> {
return this.request("/config", { ignoreAborts: true });
public async getServerConfig(
ignoreAborts = true
): Promise<ServerConfigResponse> {
return this.request("/config", {
ignoreAborts,
recordCheckpoint: false
});
}
public async getVaultSnapshot(): Promise<VaultSnapshot> {
@ -119,12 +124,14 @@ export class SyncService {
{
body,
ignoreAborts = false,
recordCheckpoint = true,
decode = async (response): Promise<T> =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Endpoint methods supply the generated protocol response type.
response.json() as Promise<T>
}: {
body?: unknown;
ignoreAborts?: boolean;
recordCheckpoint?: boolean;
decode?: (response: Response) => Promise<T>;
} = {}
): Promise<T> {
@ -169,7 +176,7 @@ export class SyncService {
}
// Finish metadata saves even if the transport is paused meanwhile.
if (!ignoreAborts) {
if (!ignoreAborts && recordCheckpoint) {
await this.recordHistory(response);
}