Fix lints & format
This commit is contained in:
parent
6d40097bcd
commit
792f57dc7e
36 changed files with 342 additions and 1687 deletions
|
|
@ -13,8 +13,6 @@ import { HttpClientError } from "../errors/http-client-error";
|
|||
import type { SerializedError } from "./types/SerializedError";
|
||||
import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent";
|
||||
import type { DocumentUpdateResponse } from "./types/DocumentUpdateResponse";
|
||||
import type { DocumentVersion } from "./types/DocumentVersion";
|
||||
import type { FetchLatestDocumentsResponse } from "./types/FetchLatestDocumentsResponse";
|
||||
import type { PingResponse } from "./types/PingResponse";
|
||||
import type { UpdateTextDocumentVersion } from "./types/UpdateTextDocumentVersion";
|
||||
import { buildVaultUrl } from "./build-vault-url";
|
||||
|
|
@ -272,32 +270,6 @@ export class SyncService {
|
|||
});
|
||||
}
|
||||
|
||||
public async get({
|
||||
documentId
|
||||
}: {
|
||||
documentId: DocumentId;
|
||||
}): Promise<DocumentVersion> {
|
||||
return this.retryForever(async () => {
|
||||
this.logger.debug(`Getting document with id ${documentId}`);
|
||||
|
||||
const response = await this.client(
|
||||
this.getUrl(`/documents/${documentId}`),
|
||||
{
|
||||
headers: this.getDefaultHeaders()
|
||||
}
|
||||
);
|
||||
|
||||
await SyncService.throwIfNotOk(response, "get document");
|
||||
|
||||
const result: DocumentVersion =
|
||||
(await response.json()) as DocumentVersion; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
this.logger.debug(`Got document ${JSON.stringify(result)}`);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public async getDocumentVersionContent({
|
||||
documentId,
|
||||
vaultUpdateId
|
||||
|
|
@ -332,36 +304,6 @@ export class SyncService {
|
|||
});
|
||||
}
|
||||
|
||||
public async getAll(
|
||||
since?: VaultUpdateId
|
||||
): Promise<FetchLatestDocumentsResponse> {
|
||||
return this.retryForever(async () => {
|
||||
this.logger.debug(
|
||||
"Getting all documents" +
|
||||
(since != null ? ` since ${since}` : "")
|
||||
);
|
||||
|
||||
const url = new URL(this.getUrl("/documents"));
|
||||
if (since !== undefined) {
|
||||
url.searchParams.append("since_update_id", since.toString());
|
||||
}
|
||||
const response = await this.client(url.toString(), {
|
||||
headers: this.getDefaultHeaders()
|
||||
});
|
||||
|
||||
await SyncService.throwIfNotOk(response, "get documents");
|
||||
|
||||
const result: FetchLatestDocumentsResponse =
|
||||
(await response.json()) as FetchLatestDocumentsResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
this.logger.debug(
|
||||
`Got ${result.latestDocuments.length} document metadata`
|
||||
);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public async ping(): Promise<PingResponse> {
|
||||
this.logger.debug("Pinging server");
|
||||
const response = await this.pingClient(this.getUrl("/ping"), {
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
|
||||
|
||||
/**
|
||||
* Response to a fetch latest documents request.
|
||||
*/
|
||||
export interface FetchLatestDocumentsResponse {
|
||||
latestDocuments: DocumentVersionWithoutContent[];
|
||||
/**
|
||||
* The update ID of the latest document in the response.
|
||||
*/
|
||||
lastUpdateId: bigint;
|
||||
}
|
||||
|
|
@ -56,13 +56,7 @@ export class SyncClient {
|
|||
private readonly contentCache: FixedSizeDocumentCache,
|
||||
private readonly serverConfig: ServerConfig,
|
||||
private readonly syncService: SyncService,
|
||||
private readonly expectedFsEvents: ExpectedFsEvents,
|
||||
private readonly persistence: PersistenceProvider<
|
||||
Partial<{
|
||||
settings: Partial<SyncSettings>;
|
||||
database: Partial<StoredSyncState>;
|
||||
}>
|
||||
>
|
||||
private readonly expectedFsEvents: ExpectedFsEvents
|
||||
) {}
|
||||
|
||||
public get syncedDocumentCount(): number {
|
||||
|
|
@ -172,7 +166,7 @@ export class SyncClient {
|
|||
// new deviceId, the server-side query would miss, and the
|
||||
// pending-but-lost create would deconflict instead of
|
||||
// binding to the doc its content was already absorbed into.
|
||||
let deviceId = state.deviceId;
|
||||
let { deviceId } = state;
|
||||
if (deviceId === undefined) {
|
||||
deviceId = createClientId();
|
||||
state = { ...state, deviceId };
|
||||
|
|
@ -269,8 +263,7 @@ export class SyncClient {
|
|||
contentCache,
|
||||
serverConfig,
|
||||
syncService,
|
||||
expectedFsEvents,
|
||||
persistence
|
||||
expectedFsEvents
|
||||
);
|
||||
|
||||
logger.info("SyncClient created successfully");
|
||||
|
|
@ -322,26 +315,6 @@ export class SyncClient {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload settings from disk overriding current in-memory settings.
|
||||
* Missing values will be filled in from DEFAULT_SETTINGS rather than
|
||||
* retaining current in-memory settings.
|
||||
*/
|
||||
public async reloadSettings(): Promise<void> {
|
||||
this.checkIfDestroyed("reloadSettings");
|
||||
|
||||
const state = (await this.persistence.load()) ?? {
|
||||
settings: undefined
|
||||
};
|
||||
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...(state.settings ?? {})
|
||||
};
|
||||
|
||||
await this.setSettings(settings);
|
||||
}
|
||||
|
||||
public async checkConnection(): Promise<NetworkConnectionStatus> {
|
||||
this.checkIfDestroyed("checkConnection");
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { describe, it } from "node:test";
|
|||
import assert from "node:assert";
|
||||
import { Logger } from "../tracing/logger";
|
||||
import { Settings } from "../persistence/settings";
|
||||
import { STORED_STATE_SCHEMA_VERSION, SyncEventQueue } from "./sync-event-queue";
|
||||
import {
|
||||
STORED_STATE_SCHEMA_VERSION,
|
||||
SyncEventQueue
|
||||
} from "./sync-event-queue";
|
||||
import { scheduleOfflineChanges } from "./offline-change-detector";
|
||||
import type { FileOperations } from "../file-operations/file-operations";
|
||||
import type { RelativePath } from "./types";
|
||||
|
|
@ -22,19 +25,20 @@ const makeQueue = async (): Promise<SyncEventQueue> => {
|
|||
);
|
||||
};
|
||||
|
||||
const makeOperations = (
|
||||
files: Record<string, Uint8Array>
|
||||
): FileOperations => {
|
||||
return {
|
||||
listFilesRecursively: async () => Object.keys(files),
|
||||
const makeOperations = (files: Record<string, Uint8Array>): FileOperations => {
|
||||
const map = new Map<RelativePath, Uint8Array>(Object.entries(files));
|
||||
const partial: Partial<FileOperations> = {
|
||||
listFilesRecursively: async () => [...map.keys()],
|
||||
read: async (path: RelativePath) => {
|
||||
const data = files[path];
|
||||
const data = map.get(path);
|
||||
if (data === undefined) {
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} as unknown as FileOperations;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return partial as FileOperations;
|
||||
};
|
||||
|
||||
describe("scheduleOfflineChanges", () => {
|
||||
|
|
@ -70,7 +74,8 @@ describe("scheduleOfflineChanges", () => {
|
|||
operations,
|
||||
queue,
|
||||
(path) => enqueued.push({ kind: "create", path }),
|
||||
(args) => enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(args) =>
|
||||
enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(path) => enqueued.push({ kind: "delete", path })
|
||||
);
|
||||
|
||||
|
|
@ -109,13 +114,12 @@ describe("scheduleOfflineChanges", () => {
|
|||
operations,
|
||||
queue,
|
||||
(path) => enqueued.push({ kind: "create", path }),
|
||||
(args) => enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(args) =>
|
||||
enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(path) => enqueued.push({ kind: "delete", path })
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(enqueued, [
|
||||
{ kind: "update", path: "doc.md" }
|
||||
]);
|
||||
assert.deepStrictEqual(enqueued, [{ kind: "update", path: "doc.md" }]);
|
||||
});
|
||||
|
||||
it("schedules a delete for a settled record whose local file is missing", async () => {
|
||||
|
|
@ -136,13 +140,12 @@ describe("scheduleOfflineChanges", () => {
|
|||
operations,
|
||||
queue,
|
||||
(path) => enqueued.push({ kind: "create", path }),
|
||||
(args) => enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(args) =>
|
||||
enqueued.push({ kind: "update", path: args.relativePath }),
|
||||
(path) => enqueued.push({ kind: "delete", path })
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(enqueued, [
|
||||
{ kind: "delete", path: "gone.md" }
|
||||
]);
|
||||
assert.deepStrictEqual(enqueued, [{ kind: "delete", path: "gone.md" }]);
|
||||
});
|
||||
|
||||
it("detects an offline rename when an untracked file matches a deleted record's content hash", async () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,24 @@ import type { SyncEventQueue } from "./sync-event-queue";
|
|||
import { removeFromArray } from "../utils/remove-from-array";
|
||||
import { FileNotFoundError } from "../errors/file-not-found-error";
|
||||
|
||||
async function readOrUndefined(
|
||||
operations: FileOperations,
|
||||
path: RelativePath,
|
||||
logger: Logger
|
||||
): Promise<Uint8Array | undefined> {
|
||||
try {
|
||||
return await operations.read(path);
|
||||
} catch (e) {
|
||||
if (e instanceof FileNotFoundError) {
|
||||
logger.debug(
|
||||
`File ${path} disappeared before offline-scan could read it; skipping`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the local filesystem and the document database to determine
|
||||
* which files were created, updated, moved, or deleted while the
|
||||
|
|
@ -85,18 +103,10 @@ export async function scheduleOfflineChanges(
|
|||
// the whole scan; nothing to sync for a file that's already gone.
|
||||
const disappearedPaths = new Set<RelativePath>();
|
||||
for (const path of locallyPossibleCreatedFiles) {
|
||||
let content: Uint8Array;
|
||||
try {
|
||||
content = await operations.read(path);
|
||||
} catch (e) {
|
||||
if (e instanceof FileNotFoundError) {
|
||||
logger.debug(
|
||||
`File ${path} disappeared before offline-scan could read it; skipping`
|
||||
);
|
||||
disappearedPaths.add(path);
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
const content = await readOrUndefined(operations, path, logger);
|
||||
if (content === undefined) {
|
||||
disappearedPaths.add(path);
|
||||
continue;
|
||||
}
|
||||
const contentHash = await hash(content);
|
||||
|
||||
|
|
@ -148,8 +158,7 @@ export async function scheduleOfflineChanges(
|
|||
for (const path of syncedLocalFiles) {
|
||||
const record = allDocuments.get(path);
|
||||
if (
|
||||
record !== undefined &&
|
||||
record.localPath !== undefined &&
|
||||
record?.localPath !== undefined &&
|
||||
record.localPath !== record.remoteRelativePath &&
|
||||
!allLocalFiles.has(record.remoteRelativePath) &&
|
||||
queue.byLocalPath.get(record.remoteRelativePath) === undefined
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { describe, it } from "node:test";
|
|||
import assert from "node:assert";
|
||||
import { Logger, LogLevel } from "../tracing/logger";
|
||||
import { Settings } from "../persistence/settings";
|
||||
import { STORED_STATE_SCHEMA_VERSION, SyncEventQueue } from "./sync-event-queue";
|
||||
import {
|
||||
STORED_STATE_SCHEMA_VERSION,
|
||||
SyncEventQueue
|
||||
} from "./sync-event-queue";
|
||||
import { Reconciler } from "./reconciler";
|
||||
import { SyncResetError } from "../errors/sync-reset-error";
|
||||
import type { FileOperations } from "../file-operations/file-operations";
|
||||
|
|
@ -32,18 +35,22 @@ describe("Reconciler", () => {
|
|||
localPath: undefined
|
||||
});
|
||||
|
||||
const operations = {
|
||||
const operationsPartial: Partial<FileOperations> = {
|
||||
exists: async () => false,
|
||||
create: async () => {
|
||||
assert.fail("reset-interrupted placement should not write");
|
||||
}
|
||||
} as unknown as FileOperations;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const operations = operationsPartial as FileOperations;
|
||||
|
||||
const syncService = {
|
||||
const syncServicePartial: Partial<SyncService> = {
|
||||
getDocumentVersionContent: async () => {
|
||||
throw new SyncResetError();
|
||||
}
|
||||
} as unknown as SyncService;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const syncService = syncServicePartial as SyncService;
|
||||
|
||||
const reconciler = new Reconciler(
|
||||
logger,
|
||||
|
|
|
|||
|
|
@ -307,7 +307,10 @@ describe("SyncEventQueue", () => {
|
|||
queue.byLocalPath.get("renamed.md" as RelativePath),
|
||||
undefined
|
||||
);
|
||||
assert.strictEqual(queue.getDocumentByDocumentId("A")?.localPath, "a.md");
|
||||
assert.strictEqual(
|
||||
queue.getDocumentByDocumentId("A")?.localPath,
|
||||
"a.md"
|
||||
);
|
||||
|
||||
// setLocalPath does re-key — it's the explicit path-mutation API.
|
||||
await queue.setLocalPath("A", "later.md" as RelativePath);
|
||||
|
|
|
|||
|
|
@ -220,9 +220,7 @@ export class SyncEventQueue {
|
|||
* path) still fires when neither side holds a record for the
|
||||
* collision target.
|
||||
*/
|
||||
public lastSeenUpdateIdForCreate(
|
||||
requestPath: RelativePath
|
||||
): VaultUpdateId {
|
||||
public lastSeenUpdateIdForCreate(requestPath: RelativePath): VaultUpdateId {
|
||||
let watermark = this._lastSeenUpdateId.min;
|
||||
for (const record of this.byDocId.values()) {
|
||||
if (
|
||||
|
|
@ -324,7 +322,7 @@ export class SyncEventQueue {
|
|||
!pendingCreate.isProcessing
|
||||
) {
|
||||
this.cancelPendingCreate(pendingCreate);
|
||||
if (recordIsDeleting && record !== undefined) {
|
||||
if (recordIsDeleting) {
|
||||
// A stale deleting record was still claiming this path.
|
||||
// The not-yet-started create/delete pair collapsed to
|
||||
// nothing, and the disk file is gone, so clear the stale
|
||||
|
|
@ -343,11 +341,11 @@ export class SyncEventQueue {
|
|||
path: lookupPath
|
||||
});
|
||||
this.notifyPendingUpdateCountChanged();
|
||||
if (recordOwnsLookupPath && record !== undefined) {
|
||||
if (recordOwnsLookupPath) {
|
||||
// The file is gone from disk; clear the doc's localPath so the
|
||||
// Reconciler doesn't try to operate on a vacated slot.
|
||||
await this.setLocalPath(record.documentId, undefined);
|
||||
} else if (recordIsDeleting && record !== undefined) {
|
||||
} else if (recordIsDeleting) {
|
||||
// A stale deleting record was still claiming this path while a
|
||||
// newer pending create owned the actual disk file. Drop the
|
||||
// stale claim now that the file is gone.
|
||||
|
|
@ -648,14 +646,6 @@ export class SyncEventQueue {
|
|||
return this.byDocId.get(target);
|
||||
}
|
||||
|
||||
public getDocumentByDocumentIdOrFail(target: DocumentId): DocumentRecord {
|
||||
const result = this.getDocumentByDocumentId(target);
|
||||
if (!result) {
|
||||
throw new Error(`No document found with id ${target}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getRecordByLocalPath(
|
||||
path: RelativePath
|
||||
): DocumentRecord | undefined {
|
||||
|
|
@ -814,6 +804,7 @@ export class SyncEventQueue {
|
|||
event.path === path &&
|
||||
event.documentId !== promise
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax -- splice-by-index here is a reorder, not an item removal
|
||||
this.events.splice(i, 1);
|
||||
this.events.splice(createIndex, 0, event);
|
||||
createIndex++;
|
||||
|
|
@ -866,6 +857,7 @@ export class SyncEventQueue {
|
|||
typeof event.documentId === "string" &&
|
||||
blockingDocIds.has(event.documentId)
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax -- splice-by-index here is a reorder, not an item removal
|
||||
this.events.splice(i, 1);
|
||||
this.events.splice(createIndex, 0, event);
|
||||
createIndex++;
|
||||
|
|
@ -907,8 +899,8 @@ export class SyncEventQueue {
|
|||
this._byLocalPath.delete(previousLocalPath);
|
||||
}
|
||||
record.localPath = newLocalPath;
|
||||
let displacedRecord: DocumentRecord | undefined;
|
||||
let displacedOldPath: RelativePath | undefined;
|
||||
let displacedRecord: DocumentRecord | undefined = undefined;
|
||||
let displacedOldPath: RelativePath | undefined = undefined;
|
||||
if (newLocalPath !== undefined) {
|
||||
const displaced = this._byLocalPath.get(newLocalPath);
|
||||
if (displaced !== undefined && displaced !== record) {
|
||||
|
|
|
|||
|
|
@ -54,11 +54,6 @@ export class Logger {
|
|||
);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.messages.length = 0;
|
||||
this.debug("Logger has been reset");
|
||||
}
|
||||
|
||||
private pushMessage(message: string, level: LogLevel): void {
|
||||
const logLine = new LogLine(level, message);
|
||||
this.messages.push(logLine);
|
||||
|
|
|
|||
|
|
@ -92,10 +92,6 @@ export class Locks<T> {
|
|||
this.waiters.clear();
|
||||
}
|
||||
|
||||
public isLocked(key: T): boolean {
|
||||
return this.locked.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to acquire a lock immediately without waiting.
|
||||
* Must call `unlock()` if successful.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue