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 {}
|
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 {}
|
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.
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export class FileChangeSession {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.checkpoint.assertUnchanged();
|
this.options.assertUnchanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public beforeFileMutation(): void {
|
public beforeFileMutation(): void {
|
||||||
|
|
@ -64,7 +64,9 @@ export class FileChangeSession {
|
||||||
const { localChanges, isProtectedPath } = this.options;
|
const { localChanges, isProtectedPath } = this.options;
|
||||||
await localChanges.flush();
|
await localChanges.flush();
|
||||||
|
|
||||||
this.abortIfStale();
|
if (!this.mutationsStarted) {
|
||||||
|
this.options.assertUnchanged();
|
||||||
|
}
|
||||||
|
|
||||||
for (const change of getUnappliedChanges(
|
for (const change of getUnappliedChanges(
|
||||||
this.planned,
|
this.planned,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { test } from "node:test";
|
||||||
import { MemoryDisk } from "../../../test-support/storage";
|
import { MemoryDisk } from "../../../test-support/storage";
|
||||||
import { Database, createEmptyDatabase } from "../persistence/database";
|
import { Database, createEmptyDatabase } from "../persistence/database";
|
||||||
import type { ServerConfig } from "../services/server-config";
|
import type { ServerConfig } from "../services/server-config";
|
||||||
import { LocalChangeCheckpoint } from "../sync-operations/local-change-checkpoint";
|
|
||||||
import { hash } from "../utils/hash";
|
import { hash } from "../utils/hash";
|
||||||
import { FileOperations } from "./file-operations";
|
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
|
async () => undefined
|
||||||
);
|
);
|
||||||
const files = new FileOperations(disk, database, {} as ServerConfig);
|
const files = new FileOperations(disk, database, {} as ServerConfig);
|
||||||
const checkpoint = new LocalChangeCheckpoint({ generation: 0 });
|
|
||||||
const renames: string[] = [];
|
const renames: string[] = [];
|
||||||
disk.boundary = (label): void => {
|
disk.boundary = (label): void => {
|
||||||
if (label.startsWith("visible:rename:")) {
|
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);
|
const next = structuredClone(database.state);
|
||||||
next.actualFileManifest = { a: "b.md", b: "a.md" };
|
next.actualFileManifest = { a: "b.md", b: "a.md" };
|
||||||
await files.applyChanges(next, checkpoint);
|
await files.applyChanges(next, () => undefined);
|
||||||
|
|
||||||
assert.deepEqual(renames, [
|
assert.deepEqual(renames, [
|
||||||
"b.md->b (conflict b).md", // Free B without losing its contents.
|
"b.md->b (conflict b).md", // Free B without losing its contents.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export interface ContentHead {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentState {
|
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 };
|
base?: { vaultUpdateId: VaultUpdateId | null; hash: string };
|
||||||
|
|
||||||
// Last observed disk hash; an empty string means a notification awaits scanning.
|
// Last observed disk hash; an empty string means a notification awaits scanning.
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,23 @@ const config = {
|
||||||
mergeableFileExtensions: ["md"],
|
mergeableFileExtensions: ["md"],
|
||||||
serverVersion: "test"
|
serverVersion: "test"
|
||||||
};
|
};
|
||||||
const make = (fetch: typeof globalThis.fetch): ServerConfig =>
|
const make = (fetch: typeof globalThis.fetch): ServerConfig => {
|
||||||
new ServerConfig(
|
const service = new SyncService(
|
||||||
new SyncService(
|
"device",
|
||||||
"device",
|
new Settings(
|
||||||
new Settings(
|
new Logger(),
|
||||||
new Logger(),
|
{ remoteUri: "http://test" },
|
||||||
{ remoteUri: "http://test" },
|
async () => undefined
|
||||||
async () => undefined
|
),
|
||||||
),
|
fetch,
|
||||||
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 () => {
|
test("configuration shares one validated request and retries failures", async () => {
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export class ServerConfig {
|
||||||
public async getConfig(): Promise<ServerConfigResponse> {
|
public async getConfig(): Promise<ServerConfigResponse> {
|
||||||
if (!this.config) {
|
if (!this.config) {
|
||||||
const pending = this.syncService
|
const pending = this.syncService
|
||||||
.getServerConfig()
|
.getServerConfig(false)
|
||||||
.then((config) => {
|
.then((config) => {
|
||||||
ServerConfig.validateConfig(config);
|
ServerConfig.validateConfig(config);
|
||||||
return config;
|
return config;
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,13 @@ export class SyncService {
|
||||||
await this.history.save(undefined);
|
await this.history.save(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getServerConfig(): Promise<ServerConfigResponse> {
|
public async getServerConfig(
|
||||||
return this.request("/config", { ignoreAborts: true });
|
ignoreAborts = true
|
||||||
|
): Promise<ServerConfigResponse> {
|
||||||
|
return this.request("/config", {
|
||||||
|
ignoreAborts,
|
||||||
|
recordCheckpoint: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getVaultSnapshot(): Promise<VaultSnapshot> {
|
public async getVaultSnapshot(): Promise<VaultSnapshot> {
|
||||||
|
|
@ -119,12 +124,14 @@ export class SyncService {
|
||||||
{
|
{
|
||||||
body,
|
body,
|
||||||
ignoreAborts = false,
|
ignoreAborts = false,
|
||||||
|
recordCheckpoint = true,
|
||||||
decode = async (response): Promise<T> =>
|
decode = async (response): Promise<T> =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Endpoint methods supply the generated protocol response type.
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Endpoint methods supply the generated protocol response type.
|
||||||
response.json() as Promise<T>
|
response.json() as Promise<T>
|
||||||
}: {
|
}: {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
ignoreAborts?: boolean;
|
ignoreAborts?: boolean;
|
||||||
|
recordCheckpoint?: boolean;
|
||||||
decode?: (response: Response) => Promise<T>;
|
decode?: (response: Response) => Promise<T>;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|
@ -169,7 +176,7 @@ export class SyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish metadata saves even if the transport is paused meanwhile.
|
// Finish metadata saves even if the transport is paused meanwhile.
|
||||||
if (!ignoreAborts) {
|
if (!ignoreAborts && recordCheckpoint) {
|
||||||
await this.recordHistory(response);
|
await this.recordHistory(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue