Comments & make ping refreshable
This commit is contained in:
parent
727c4b80ef
commit
b78ab61992
7 changed files with 38 additions and 22 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -13,19 +13,23 @@ const config = {
|
|||
mergeableFileExtensions: ["md"],
|
||||
serverVersion: "test"
|
||||
};
|
||||
const make = (fetch: typeof globalThis.fetch): ServerConfig =>
|
||||
new ServerConfig(
|
||||
new SyncService(
|
||||
"device",
|
||||
new Settings(
|
||||
new Logger(),
|
||||
{ remoteUri: "http://test" },
|
||||
async () => undefined
|
||||
),
|
||||
fetch,
|
||||
{ get: () => undefined, save: async () => undefined }
|
||||
)
|
||||
const make = (fetch: typeof globalThis.fetch): ServerConfig => {
|
||||
const service = new SyncService(
|
||||
"device",
|
||||
new Settings(
|
||||
new Logger(),
|
||||
{ remoteUri: "http://test" },
|
||||
async () => undefined
|
||||
),
|
||||
fetch,
|
||||
{
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue