diff --git a/.vscode/settings.json b/.vscode/settings.json index e5963c20..98187650 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,4 @@ "**/.sqlx": true, "**/target": true } -} \ No newline at end of file +} diff --git a/frontend/deterministic-tests/README.md b/frontend/deterministic-tests/README.md index 678cd0fe..0fe053f0 100644 --- a/frontend/deterministic-tests/README.md +++ b/frontend/deterministic-tests/README.md @@ -17,20 +17,25 @@ All tests run in parallel up to a concurrency limit. Clients always start with syncing disabled. **File operations** (per-client, fire-and-forget — sync is enqueued but not awaited): + - `create`, `update`, `rename`, `delete` **Sync control:** + - `sync` — wait for a specific client or all clients to finish pending operations - `barrier` — retry until all clients converge to identical file state (60s timeout) - `enable-sync` / `disable-sync` — simulate going online/offline **WebSocket control** (per-client): + - `pause-websocket` / `resume-websocket` — buffer/release WebSocket messages for a specific client **Server control:** + - `pause-server` / `resume-server` — SIGSTOP/SIGCONT the server process **Assertions:** + - `assert-consistent` — all clients have identical files; optionally takes a custom `verify(state: AssertableState)` callback ## Running @@ -57,15 +62,19 @@ npm run test -w deterministic-tests -- -j 4 import type { TestDefinition } from "../test-definition"; export const myScenarioTest: TestDefinition = { - description: "Client 0 creates A.md offline. After syncing, both clients should have the file.", - clients: 2, - steps: [ - { type: "create", client: 0, path: "A.md", content: "hello" }, - { type: "enable-sync", client: 0 }, - { type: "enable-sync", client: 1 }, - { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("A.md", "hello") } - ] + description: + "Client 0 creates A.md offline. After syncing, both clients should have the file.", + clients: 2, + steps: [ + { type: "create", client: 0, path: "A.md", content: "hello" }, + { type: "enable-sync", client: 0 }, + { type: "enable-sync", client: 1 }, + { type: "barrier" }, + { + type: "assert-consistent", + verify: (s) => s.assertFileCount(1).assertContent("A.md", "hello") + } + ] }; ``` @@ -88,7 +97,7 @@ s.ifFileExists("path", (s) => ...) // conditional assertion import { myScenarioTest } from "./tests/my-scenario.test"; const TESTS = { - // ... - "my-scenario": myScenarioTest + // ... + "my-scenario": myScenarioTest }; ``` diff --git a/frontend/deterministic-tests/src/cli.ts b/frontend/deterministic-tests/src/cli.ts index 57cee963..6e0e764f 100644 --- a/frontend/deterministic-tests/src/cli.ts +++ b/frontend/deterministic-tests/src/cli.ts @@ -38,137 +38,6 @@ interface NamedTestResult { result: TestResult; } - -async function main(): Promise { - const cwd = process.cwd(); - let projectRoot = cwd; - - if (cwd.endsWith("frontend/deterministic-tests")) { - projectRoot = path.resolve(cwd, "../.."); - } else if (cwd.endsWith("frontend")) { - projectRoot = path.resolve(cwd, ".."); - } - - const serverPath = path.join(projectRoot, SERVER_BINARY_PATH); - if (!fs.existsSync(serverPath)) { - logger.error(`Server binary not found at: ${serverPath}`); - process.exit(1); - } - - const configPath = path.join(projectRoot, CONFIG_PATH); - if (!fs.existsSync(configPath)) { - logger.error(`Config file not found at: ${configPath}`); - process.exit(1); - } - - const filterArg = process.argv.find((a) => a.startsWith("--filter=")); - const filter = filterArg?.slice("--filter=".length); - - const testsToRun: [string, TestDefinition][] = []; - for (const [key, test] of Object.entries(TESTS)) { - if (test) { - if (filter && !key.includes(filter)) { - continue; - } - testsToRun.push([key, test]); - } - } - - if (testsToRun.length === 0) { - logger.error( - filter - ? `No tests matched filter "${filter}"` - : "No tests found" - ); - process.exit(1); - } - - const concurrency = parseConcurrency(); - const regularTests = testsToRun.filter( - ([, t]) => !testUsesPauseServer(t) - ); - const pauseTests = testsToRun.filter(([, t]) => testUsesPauseServer(t)); - - logger.info(`Server: ${serverPath}`); - logger.info(`Config: ${configPath}`); - logger.info( - `Tests: ${testsToRun.length} total (${regularTests.length} regular, ${pauseTests.length} server-pause)` - ); - logger.info(`Concurrency: ${concurrency}`); - - const allResults: NamedTestResult[] = []; - - if (regularTests.length > 0) { - logger.info( - `\n--- Running ${regularTests.length} regular tests (shared server, concurrency ${concurrency}) ---` - ); - const sharedServer = new ServerControl( - serverPath, - configPath, - logger - ); - serverManager.track(sharedServer); - - try { - await sharedServer.start(); - - const results = await runWithConcurrency( - regularTests, - concurrency, - async ([name, test]) => - runSharedServerTest(name, test, sharedServer) - ); - - allResults.push(...results); - } finally { - try { - await sharedServer.stop(); - } catch (error) { - logger.warn( - `Error stopping shared server: ${error instanceof Error ? error.message : String(error)}` - ); - } - serverManager.untrack(sharedServer); - } - } - - if (pauseTests.length > 0) { - logger.info( - `\n--- Running ${pauseTests.length} server-pause tests (dedicated servers, concurrency ${concurrency}) ---` - ); - - const results = await runWithConcurrency( - pauseTests, - concurrency, - async ([name, test]) => - runDedicatedServerTest(name, test, serverPath, configPath) - ); - - allResults.push(...results); - } - - const passed = allResults.filter((r) => r.result.success); - const failed = allResults.filter((r) => !r.result.success); - - logger.info(`\n--- Results: ${passed.length}/${allResults.length} passed ---`); - - if (failed.length > 0) { - for (const { name, result } of failed) { - logger.error(` FAILED: ${name}: ${result.error}`); - } - process.exit(1); - } else { - logger.info("All tests passed!"); - process.exit(0); - } -} - -main().catch((err: unknown) => { - logger.error(`Unexpected error: ${err}`); - process.exit(1); -}); - - async function runSharedServerTest( name: string, test: TestDefinition, @@ -229,3 +98,132 @@ async function runDedicatedServerTest( serverManager.untrack(server); } } + +async function main(): Promise { + const cwd = process.cwd(); + let projectRoot = cwd; + + if (cwd.endsWith("frontend/deterministic-tests")) { + projectRoot = path.resolve(cwd, "../.."); + } else if (cwd.endsWith("frontend")) { + projectRoot = path.resolve(cwd, ".."); + } + + const serverPath = path.join(projectRoot, SERVER_BINARY_PATH); + if (!fs.existsSync(serverPath)) { + logger.error(`Server binary not found at: ${serverPath}`); + process.exit(1); + } + + const configPath = path.join(projectRoot, CONFIG_PATH); + if (!fs.existsSync(configPath)) { + logger.error(`Config file not found at: ${configPath}`); + process.exit(1); + } + + const filterArg = process.argv.find((a) => a.startsWith("--filter=")); + const filter = filterArg?.slice("--filter=".length); + + const testsToRun: [string, TestDefinition][] = []; + for (const [key, test] of Object.entries(TESTS)) { + if (test) { + if ( + filter !== undefined && + filter.length > 0 && + !key.includes(filter) + ) { + continue; + } + testsToRun.push([key, test]); + } + } + + if (testsToRun.length === 0) { + logger.error( + filter !== undefined && filter.length > 0 + ? `No tests matched filter "${filter}"` + : "No tests found" + ); + process.exit(1); + } + + const concurrency = parseConcurrency(); + const regularTests = testsToRun.filter(([, t]) => !testUsesPauseServer(t)); + const pauseTests = testsToRun.filter(([, t]) => testUsesPauseServer(t)); + + logger.info(`Server: ${serverPath}`); + logger.info(`Config: ${configPath}`); + logger.info( + `Tests: ${testsToRun.length} total (${regularTests.length} regular, ${pauseTests.length} server-pause)` + ); + logger.info(`Concurrency: ${concurrency}`); + + const allResults: NamedTestResult[] = []; + + if (regularTests.length > 0) { + logger.info( + `\n--- Running ${regularTests.length} regular tests (shared server, concurrency ${concurrency}) ---` + ); + const sharedServer = new ServerControl(serverPath, configPath, logger); + serverManager.track(sharedServer); + + try { + await sharedServer.start(); + + const results = await runWithConcurrency( + regularTests, + concurrency, + async ([name, test]) => + runSharedServerTest(name, test, sharedServer) + ); + + allResults.push(...results); + } finally { + try { + await sharedServer.stop(); + } catch (error) { + logger.warn( + `Error stopping shared server: ${error instanceof Error ? error.message : String(error)}` + ); + } + serverManager.untrack(sharedServer); + } + } + + if (pauseTests.length > 0) { + logger.info( + `\n--- Running ${pauseTests.length} server-pause tests (dedicated servers, concurrency ${concurrency}) ---` + ); + + const results = await runWithConcurrency( + pauseTests, + concurrency, + async ([name, test]) => + runDedicatedServerTest(name, test, serverPath, configPath) + ); + + allResults.push(...results); + } + + const passed = allResults.filter((r) => r.result.success); + const failed = allResults.filter((r) => !r.result.success); + + logger.info( + `\n--- Results: ${passed.length}/${allResults.length} passed ---` + ); + + if (failed.length > 0) { + for (const { name, result } of failed) { + logger.error(` FAILED: ${name}: ${result.error}`); + } + process.exit(1); + } else { + logger.info("All tests passed!"); + process.exit(0); + } +} + +main().catch((err: unknown) => { + logger.error(`Unexpected error: ${err}`); + process.exit(1); +}); diff --git a/frontend/deterministic-tests/src/deterministic-agent.ts b/frontend/deterministic-tests/src/deterministic-agent.ts index 71f6a272..f253186a 100644 --- a/frontend/deterministic-tests/src/deterministic-agent.ts +++ b/frontend/deterministic-tests/src/deterministic-agent.ts @@ -1,13 +1,21 @@ -import type { StoredDatabase, SyncSettings, RelativePath, TextWithCursors } from "sync-client"; -import { SyncClient, debugging, LogLevel } from "sync-client"; +import type { + StoredDatabase, + SyncSettings, + RelativePath, + TextWithCursors +} from "sync-client"; +import { SyncClient, debugging, LogLevel, utils } from "sync-client"; import { assert } from "./utils/assert"; import { sleep } from "./utils/sleep"; import { withTimeout } from "./utils/with-timeout"; -import { IS_SYNC_ENABLED_BY_DEFAULT, WAIT_TIMEOUT_MS, WEBSOCKET_CONNECT_TIMEOUT_MS, WEBSOCKET_POLL_INTERVAL_MS } from "./consts"; +import { + IS_SYNC_ENABLED_BY_DEFAULT, + WAIT_TIMEOUT_MS, + WEBSOCKET_CONNECT_TIMEOUT_MS, + WEBSOCKET_POLL_INTERVAL_MS +} from "./consts"; import { ManagedWebSocketFactory } from "./managed-websocket"; - - export class DeterministicAgent extends debugging.InMemoryFileSystem { public readonly clientId: number; private readonly logger: (msg: string) => void; @@ -33,7 +41,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { } public async init( - fetchImplementation: typeof globalThis.fetch, + fetchImplementation: typeof globalThis.fetch ): Promise { this.client = await SyncClient.create({ fs: this, @@ -138,7 +146,6 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { await this.waitForWebSocket(); } - public async getFileContent(path: string): Promise { const bytes = await this.read(path); return new TextDecoder().decode(bytes); @@ -146,9 +153,11 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { public async cleanup(): Promise { this.log("Cleaning up..."); - // Guard against uninitialized client (init() failed partway) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!this.client) { + // Guard against uninitialized client (init() failed partway). + // The class field uses `!:` so TS thinks this is always defined, + // but at runtime it can be undefined when init() throws partway. + const maybeClient = this.client as SyncClient | undefined; + if (maybeClient === undefined) { this.log("Client not initialized, nothing to clean up"); return; } @@ -184,11 +193,13 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { await super.write(path, content); if (isNew) { - this.enqueueSync(async () => { this.client.syncLocallyCreatedFile(path); } - ); + this.enqueueSync(async () => { + this.client.syncLocallyCreatedFile(path); + }); } else { - this.enqueueSync(async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); } - ); + this.enqueueSync(async () => { + this.client.syncLocallyUpdatedFile({ relativePath: path }); + }); } } @@ -197,18 +208,18 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { updater: (current: TextWithCursors) => TextWithCursors ): Promise { const result = await super.atomicUpdateText(path, updater); - this.enqueueSync(async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); } - ); + this.enqueueSync(async () => { + this.client.syncLocallyUpdatedFile({ relativePath: path }); + }); return result; - } - public override async delete(path: RelativePath): Promise { await super.delete(path); if (this.isSyncEnabled) { - this.enqueueSync(async () => { this.client.syncLocallyDeletedFile(path); } - ); + this.enqueueSync(async () => { + this.client.syncLocallyDeletedFile(path); + }); } } @@ -222,8 +233,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { oldPath, relativePath: newPath }); - } - ); + }); } private async waitForWebSocket(): Promise { @@ -243,7 +253,7 @@ export class DeterministicAgent extends debugging.InMemoryFileSystem { */ private async drainPendingSyncOperations(): Promise { while (this.pendingSyncOperations.size > 0) { - await Promise.all(this.pendingSyncOperations); + await utils.awaitAll([...this.pendingSyncOperations]); } } diff --git a/frontend/deterministic-tests/src/managed-websocket.ts b/frontend/deterministic-tests/src/managed-websocket.ts index c09b44d7..c97a43a0 100644 --- a/frontend/deterministic-tests/src/managed-websocket.ts +++ b/frontend/deterministic-tests/src/managed-websocket.ts @@ -2,16 +2,129 @@ * A WebSocket wrapper that can pause and resume message delivery. * When paused, incoming messages are buffered. When resumed, buffered * messages are delivered in order via the onmessage handler. + * + * Member layout follows typescript-eslint default member-ordering: all + * accessor properties are declared with `declare` and wired through the + * constructor using Object.defineProperty so we don't need conflicting + * get/set accessor pairs. */ export class ManagedWebSocket implements WebSocket { + public static readonly CONNECTING = WebSocket.CONNECTING; + public static readonly OPEN = WebSocket.OPEN; + public static readonly CLOSING = WebSocket.CLOSING; + public static readonly CLOSED = WebSocket.CLOSED; + + public readonly CONNECTING = WebSocket.CONNECTING; + public readonly OPEN = WebSocket.OPEN; + public readonly CLOSING = WebSocket.CLOSING; + public readonly CLOSED = WebSocket.CLOSED; + + declare public readonly readyState: number; + declare public readonly url: string; + declare public readonly protocol: string; + declare public readonly extensions: string; + declare public readonly bufferedAmount: number; + declare public binaryType: BinaryType; + declare public onopen: ((this: WebSocket, ev: Event) => unknown) | null; + declare public onclose: + | ((this: WebSocket, ev: CloseEvent) => unknown) + | null; + declare public onerror: ((this: WebSocket, ev: Event) => unknown) | null; + declare public onmessage: + | ((this: WebSocket, ev: MessageEvent) => unknown) + | null; + private readonly ws: WebSocket; - private paused = false; private readonly bufferedMessages: MessageEvent[] = []; + private paused = false; private externalOnMessage: ((event: MessageEvent) => unknown) | null = null; public constructor(url: string | URL, protocols?: string | string[]) { this.ws = new WebSocket(url, protocols); + const { ws } = this; + Object.defineProperties(this, { + readyState: { + get: (): number => ws.readyState, + enumerable: true, + configurable: true + }, + url: { + get: (): string => ws.url, + enumerable: true, + configurable: true + }, + protocol: { + get: (): string => ws.protocol, + enumerable: true, + configurable: true + }, + extensions: { + get: (): string => ws.extensions, + enumerable: true, + configurable: true + }, + bufferedAmount: { + get: (): number => ws.bufferedAmount, + enumerable: true, + configurable: true + }, + binaryType: { + get: (): BinaryType => ws.binaryType, + set: (v: BinaryType): void => { + ws.binaryType = v; + }, + enumerable: true, + configurable: true + }, + onopen: { + get: (): ((this: WebSocket, ev: Event) => unknown) | null => + ws.onopen, + set: ( + h: ((this: WebSocket, ev: Event) => unknown) | null + ): void => { + ws.onopen = h; + }, + enumerable: true, + configurable: true + }, + onclose: { + get: (): + | ((this: WebSocket, ev: CloseEvent) => unknown) + | null => ws.onclose, + set: ( + h: ((this: WebSocket, ev: CloseEvent) => unknown) | null + ): void => { + ws.onclose = h; + }, + enumerable: true, + configurable: true + }, + onerror: { + get: (): ((this: WebSocket, ev: Event) => unknown) | null => + ws.onerror, + set: ( + h: ((this: WebSocket, ev: Event) => unknown) | null + ): void => { + ws.onerror = h; + }, + enumerable: true, + configurable: true + }, + onmessage: { + get: (): + | ((this: WebSocket, ev: MessageEvent) => unknown) + | null => this.externalOnMessage, + set: ( + h: ((this: WebSocket, ev: MessageEvent) => unknown) | null + ): void => { + this.externalOnMessage = h; + }, + enumerable: true, + configurable: true + } + }); + this.ws.onmessage = (event: MessageEvent): void => { if (this.paused) { this.bufferedMessages.push(event); @@ -33,68 +146,6 @@ export class ManagedWebSocket implements WebSocket { } } - get readyState(): number { - return this.ws.readyState; - } - - get url(): string { - return this.ws.url; - } - - get protocol(): string { - return this.ws.protocol; - } - - get extensions(): string { - return this.ws.extensions; - } - - get bufferedAmount(): number { - return this.ws.bufferedAmount; - } - - get binaryType(): BinaryType { - return this.ws.binaryType; - } - - set binaryType(value: BinaryType) { - this.ws.binaryType = value; - } - - get onopen(): ((this: WebSocket, ev: Event) => unknown) | null { - return this.ws.onopen; - } - - set onopen(handler: ((this: WebSocket, ev: Event) => unknown) | null) { - this.ws.onopen = handler; - } - - get onclose(): ((this: WebSocket, ev: CloseEvent) => unknown) | null { - return this.ws.onclose; - } - - set onclose(handler: ((this: WebSocket, ev: CloseEvent) => unknown) | null) { - this.ws.onclose = handler; - } - - get onerror(): ((this: WebSocket, ev: Event) => unknown) | null { - return this.ws.onerror; - } - - set onerror(handler: ((this: WebSocket, ev: Event) => unknown) | null) { - this.ws.onerror = handler; - } - - get onmessage(): ((this: WebSocket, ev: MessageEvent) => unknown) | null { - return this.externalOnMessage; - } - - set onmessage( - handler: ((this: WebSocket, ev: MessageEvent) => unknown) | null - ) { - this.externalOnMessage = handler; - } - public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { this.ws.send(data); } @@ -118,16 +169,6 @@ export class ManagedWebSocket implements WebSocket { public dispatchEvent(event: Event): boolean { return this.ws.dispatchEvent(event); } - - static readonly CONNECTING = WebSocket.CONNECTING; - static readonly OPEN = WebSocket.OPEN; - static readonly CLOSING = WebSocket.CLOSING; - static readonly CLOSED = WebSocket.CLOSED; - - readonly CONNECTING = WebSocket.CONNECTING; - readonly OPEN = WebSocket.OPEN; - readonly CLOSING = WebSocket.CLOSING; - readonly CLOSED = WebSocket.CLOSED; } /** @@ -138,22 +179,19 @@ export class ManagedWebSocketFactory { private readonly instances: ManagedWebSocket[] = []; public get constructorFn(): typeof globalThis.WebSocket { - const factory = this; - const ctor = function ManagedWS( - url: string | URL, - protocols?: string | string[] - ): ManagedWebSocket { - const ws = new ManagedWebSocket(url, protocols); - factory.instances.push(ws); - return ws; - } as unknown as typeof globalThis.WebSocket; - - Object.defineProperty(ctor, "CONNECTING", { value: WebSocket.CONNECTING }); - Object.defineProperty(ctor, "OPEN", { value: WebSocket.OPEN }); - Object.defineProperty(ctor, "CLOSING", { value: WebSocket.CLOSING }); - Object.defineProperty(ctor, "CLOSED", { value: WebSocket.CLOSED }); - - return ctor; + const trackInstance = (instance: ManagedWebSocket): void => { + this.instances.push(instance); + }; + class TrackedManagedWebSocket extends ManagedWebSocket { + public constructor( + url: string | URL, + protocols?: string | string[] + ) { + super(url, protocols); + trackInstance(this); + } + } + return TrackedManagedWebSocket; } public pause(): void { diff --git a/frontend/deterministic-tests/src/server-control.ts b/frontend/deterministic-tests/src/server-control.ts index de0dbe4b..f903cc4c 100644 --- a/frontend/deterministic-tests/src/server-control.ts +++ b/frontend/deterministic-tests/src/server-control.ts @@ -42,9 +42,7 @@ export class ServerControl { this._port = reservation.port; // Prefer tmpfs (/host/tmp) over disk-backed /tmp for faster SQLite I/O const tmpBase = fs.existsSync("/host/tmp") ? "/host/tmp" : os.tmpdir(); - this.tempDir = fs.mkdtempSync( - path.join(tmpBase, "vault-link-test-") - ); + this.tempDir = fs.mkdtempSync(path.join(tmpBase, "vault-link-test-")); const tempConfigPath = path.join(this.tempDir, "config.yml"); const dbDir = path.join(this.tempDir, "databases"); @@ -225,7 +223,7 @@ export class ServerControl { } private cleanupTempDir(): void { - if (this.tempDir) { + if (this.tempDir !== undefined) { try { fs.rmSync(this.tempDir, { recursive: true, force: true }); } catch { @@ -234,5 +232,4 @@ export class ServerControl { this.tempDir = undefined; } } - } diff --git a/frontend/deterministic-tests/src/server-manager.ts b/frontend/deterministic-tests/src/server-manager.ts index 8764e669..e9ca3d57 100644 --- a/frontend/deterministic-tests/src/server-manager.ts +++ b/frontend/deterministic-tests/src/server-manager.ts @@ -39,14 +39,18 @@ export class ServerManager { process.on("SIGINT", () => { this.logger.info("Received SIGINT, shutting down..."); void this.stopAll() - .catch(() => {}) + .catch(() => { + /* no-op */ + }) .then(() => process.exit(130)); }); process.on("SIGTERM", () => { this.logger.info("Received SIGTERM, shutting down..."); void this.stopAll() - .catch(() => {}) + .catch(() => { + /* no-op */ + }) .then(() => process.exit(143)); }); } diff --git a/frontend/deterministic-tests/src/test-registry.ts b/frontend/deterministic-tests/src/test-registry.ts index 4a16db2b..36089335 100644 --- a/frontend/deterministic-tests/src/test-registry.ts +++ b/frontend/deterministic-tests/src/test-registry.ts @@ -102,10 +102,12 @@ export const TESTS: Partial> = { "delete-recreate-same-path": deleteRecreateSamePathTest, "offline-rename-and-edit": offlineRenameAndEditTest, "rename-to-existing-path": renameToExistingPathTest, - "simultaneous-create-delete-same-path": simultaneousCreateDeleteSamePathTest, + "simultaneous-create-delete-same-path": + simultaneousCreateDeleteSamePathTest, "idempotency-after-server-pause": idempotencyAfterServerPauseTest, "sequential-create-duplicate-content": sequentialCreateDuplicateContentTest, - "mc-three-client-rename-offline-update": mcThreeClientRenameOfflineUpdateTest, + "mc-three-client-rename-offline-update": + mcThreeClientRenameOfflineUpdateTest, "mc-multi-delete-offline-rename": mcMultiDeleteOfflineRenameTest, "mc-cross-create-rename-same-target": mcCrossCreateRenameSameTargetTest, "mc-delete-then-offline-rename": mcDeleteThenOfflineRenameTest, @@ -117,7 +119,8 @@ export const TESTS: Partial> = { "rename-swap": renameSwapTest, "rename-circular": renameCircularTest, "rename-roundtrip": renameRoundtripTest, - "offline-rename-remote-create-old-path": offlineRenameRemoteCreateOldPathTest, + "offline-rename-remote-create-old-path": + offlineRenameRemoteCreateOldPathTest, "offline-edit-remote-rename": offlineEditRemoteRenameTest, "rename-chain-then-delete": renameChainThenDeleteTest, "offline-delete-remote-rename": offlineDeleteRemoteRenameTest, @@ -140,34 +143,45 @@ export const TESTS: Partial> = { "delete-recreate-different-content": deleteRecreateDifferentContentTest, "update-during-create-processing": updateDuringCreateProcessingTest, "offline-move-then-remote-delete": offlineMoveThenRemoteDeleteTest, - "reset-clears-recently-deleted-resurrection": resetClearsRecentlyDeletedResurrectionTest, + "reset-clears-recently-deleted-resurrection": + resetClearsRecentlyDeletedResurrectionTest, "move-then-delete-stale-path": moveThenDeleteStalePathTest, "offline-delete-vs-remote-update": offlineDeleteVsRemoteUpdateTest, "interrupted-delete-retry": interruptedDeleteRetryTest, "update-survives-remote-delete": updateDoesNotSurvivesRemoteDeleteTest, "move-preserves-remote-update": movePreservesRemoteUpdateTest, - "recently-deleted-cleared-on-reconnect": recentlyDeletedClearedOnReconnectTest, + "recently-deleted-cleared-on-reconnect": + recentlyDeletedClearedOnReconnectTest, "migrate-key-preserves-existing": migrateKeyPreservesExistingTest, "failed-vfs-move-falls-back": failedVfsMoveFallsBackTest, "watermark-advances-on-skip": watermarkAdvancesOnSkipTest, - "watermark-gap-remote-update-not-recorded": watermarkGapRemoteUpdateNotRecordedTest, - "queue-reset-loses-coalesced-local-edit": queueResetLosesCoalescedLocalEditTest, + "watermark-gap-remote-update-not-recorded": + watermarkGapRemoteUpdateNotRecordedTest, + "queue-reset-loses-coalesced-local-edit": + queueResetLosesCoalescedLocalEditTest, "rename-to-pending-path-fallback": renameToPendingPathFallbackTest, "move-remote-update-reverts-rename": moveRemoteUpdateRevertsRenameTest, "local-edit-lost-during-create-merge": localEditLostDuringCreateMergeTest, - "rename-pending-create-before-response": renamePendingCreateBeforeResponseTest, + "rename-pending-create-before-response": + renamePendingCreateBeforeResponseTest, "create-rename-response-skips-file": createRenameResponseSkipsFileTest, - "online-create-rename-concurrent-create-orphan": onlineCreateRenameConcurrentCreateOrphanTest, + "online-create-rename-concurrent-create-orphan": + onlineCreateRenameConcurrentCreateOrphanTest, "concurrent-rename-first-wins": concurrentRenameFirstWinsTest, "binary-to-text-transition": binaryToTextTransitionTest, "text-pending-create-not-displaced": textPendingCreateNotDisplacedTest, "binary-pending-create-not-displaced": binaryPendingCreateNotDisplacedTest, - "coalesce-update-remote-update-data-loss": coalesceUpdateRemoteUpdateDataLossTest, - "coalesced-remote-update-watermark-loss": coalescedRemoteUpdateWatermarkLossTest, - "concurrent-delete-during-remote-update": concurrentDeleteDuringRemoteUpdateTest, + "coalesce-update-remote-update-data-loss": + coalesceUpdateRemoteUpdateDataLossTest, + "coalesced-remote-update-watermark-loss": + coalescedRemoteUpdateWatermarkLossTest, + "concurrent-delete-during-remote-update": + concurrentDeleteDuringRemoteUpdateTest, "concurrent-edit-exact-same-position": concurrentEditExactSamePositionTest, - "concurrent-rename-and-create-at-target-rename-first": concurrentRenameAndCreateAtTargetRenameFirstTest, - "concurrent-rename-and-create-at-target-create-first": concurrentRenameAndCreateAtTargetCreateFirstTest, + "concurrent-rename-and-create-at-target-rename-first": + concurrentRenameAndCreateAtTargetRenameFirstTest, + "concurrent-rename-and-create-at-target-create-first": + concurrentRenameAndCreateAtTargetCreateFirstTest, "concurrent-rename-same-target": concurrentRenameSameTargetTest, "concurrent-update-diff-consistency": concurrentUpdateDiffConsistencyTest, "user-parenthesized-file-not-deleted": userParenthesizedFileNotDeletedTest, @@ -176,15 +190,19 @@ export const TESTS: Partial> = { "move-identical-content-ambiguity": moveIdenticalContentAmbiguityTest, "create-update-coalesce-server-pause": createUpdateCoalesceServerPauseTest, "create-during-reconciliation": createDuringReconciliationTest, - "create-merge-preserves-renamed-update": createMergePreservesRenamedUpdateTest, + "create-merge-preserves-renamed-update": + createMergePreservesRenamedUpdateTest, "create-rename-create-same-path": createRenameCreateSamePathTest, "move-chain-three-files": moveChainThreeFilesTest, "delete-by-other-client-then-recreate": deleteByOtherClientThenRecreateTest, "online-delete-recreate-rapid-cycle": onlineDeleteRecreateRapidCycleTest, "online-edit-vs-delete-convergence": onlineEditVsDeleteConvergenceTest, - "rapid-edit-delete-online-convergence": rapidEditDeleteOnlineConvergenceTest, + "rapid-edit-delete-online-convergence": + rapidEditDeleteOnlineConvergenceTest, "server-pause-delete-recreate": serverPauseDeleteRecreateTest, - "online-both-create-same-path-deconflict": onlineBothCreateSamePathDeconflictTest, - "online-create-update-while-other-creates-same-path": onlineCreateUpdateWhileOtherCreatesSamePathTest, - "displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest, + "online-both-create-same-path-deconflict": + onlineBothCreateSamePathDeconflictTest, + "online-create-update-while-other-creates-same-path": + onlineCreateUpdateWhileOtherCreatesSamePathTest, + "displaced-file-not-marked-deleted": displacedFileNotMarkedDeletedTest }; diff --git a/frontend/deterministic-tests/src/test-runner.ts b/frontend/deterministic-tests/src/test-runner.ts index 2d469fa2..8fdefcbe 100644 --- a/frontend/deterministic-tests/src/test-runner.ts +++ b/frontend/deterministic-tests/src/test-runner.ts @@ -1,8 +1,4 @@ -import type { - TestDefinition, - TestResult, - TestStep -} from "./test-definition"; +import type { TestDefinition, TestResult, TestStep } from "./test-definition"; import { DeterministicAgent } from "./deterministic-agent"; import type { ServerControl } from "./server-control"; import type { SyncSettings, Logger } from "sync-client"; @@ -113,9 +109,7 @@ export class TestRunner { // Push before init so cleanup() handles this agent if init fails this.agents.push(agent); await withTimeout( - agent.init( - fetch, - ), + agent.init(fetch), AGENT_INIT_TIMEOUT_MS, `Client ${i} init timed out after ${AGENT_INIT_TIMEOUT_MS}ms` ); @@ -276,7 +270,10 @@ export class TestRunner { verify?: (state: AssertableState) => void ): Promise { this.logger.info("Asserting all clients are consistent..."); - assert(this.agents.length >= 2, "Need at least 2 agents for consistency check"); + assert( + this.agents.length >= 2, + "Need at least 2 agents for consistency check" + ); // Snapshot all agents' file states upfront to minimize the window // where background sync could mutate state between reads. diff --git a/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts index fced7c5f..28243525 100644 --- a/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/1-text-pending-create-not-displaced.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const textPendingCreateNotDisplacedTest: TestDefinition = { @@ -23,6 +24,13 @@ export const textPendingCreateNotDisplacedTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileExists("data.txt").assertAnyFileContains("client-0", "client-1") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1) + .assertFileExists("data.txt") + .assertAnyFileContains("client-0", "client-1"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts index 94e6914e..d21ce16b 100644 --- a/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts +++ b/frontend/deterministic-tests/src/tests/10-concurrent-update-diff-consistency.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentUpdateDiffConsistencyTest: TestDefinition = { @@ -35,6 +36,16 @@ export const concurrentUpdateDiffConsistencyTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "header by 0\nmiddle\nfooter by 1") } + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileCount(1) + .assertContent( + "doc.md", + "header by 0\nmiddle\nfooter by 1" + ); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts b/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts index 8be438e2..ef6cd771 100644 --- a/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts +++ b/frontend/deterministic-tests/src/tests/10-user-parenthesized-file-not-deleted.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const userParenthesizedFileNotDeletedTest: TestDefinition = { @@ -34,7 +35,7 @@ export const userParenthesizedFileNotDeletedTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(3) .assertFileExists("Chapter.bin") diff --git a/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts index b1239217..6c766001 100644 --- a/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts +++ b/frontend/deterministic-tests/src/tests/11-create-delete-noop.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createDeleteNoopTest: TestDefinition = { @@ -16,6 +17,11 @@ export const createDeleteNoopTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileNotExists("temp.md") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileNotExists("temp.md"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts index 4b121939..ef7ea5c3 100644 --- a/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/12-create-merge-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createMergeDeleteTest: TestDefinition = { @@ -16,12 +17,21 @@ export const createMergeDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => state.assertFileCount(1).assertContains("A.md", "from-zero", "from-one") + verify: (state: AssertableState): void => { + state + .assertFileCount(1) + .assertContains("A.md", "from-zero", "from-one"); + } }, { type: "delete", client: 0, path: "A.md" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("A.md") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(0).assertFileNotExists("A.md"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts index 9c0f7245..2a9ce0b4 100644 --- a/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts +++ b/frontend/deterministic-tests/src/tests/13-move-identical-content-ambiguity.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveIdenticalContentAmbiguityTest: TestDefinition = { @@ -31,7 +32,7 @@ export const moveIdenticalContentAmbiguityTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(1) .assertFileNotExists("A.md") diff --git a/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts b/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts index 608f845d..9b752d05 100644 --- a/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts +++ b/frontend/deterministic-tests/src/tests/15-create-update-coalesce-server-pause.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createUpdateCoalesceServerPauseTest: TestDefinition = { @@ -19,6 +20,13 @@ export const createUpdateCoalesceServerPauseTest: TestDefinition = { { type: "barrier" }, - { type: "assert-consistent", verify: (state) => state.assertFileCount(1).assertContent("doc.md", "final version") } + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileCount(1) + .assertContent("doc.md", "final version"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts b/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts index 54dc3f98..0fe51106 100644 --- a/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts +++ b/frontend/deterministic-tests/src/tests/16-create-during-reconciliation.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createDuringReconciliationTest: TestDefinition = { @@ -37,7 +38,7 @@ export const createDuringReconciliationTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(3) .assertContent("A.md", "offline A") diff --git a/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts b/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts index f600c40e..f2b6ba62 100644 --- a/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts +++ b/frontend/deterministic-tests/src/tests/17-create-merge-preserves-renamed-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createMergePreservesRenamedUpdateTest: TestDefinition = { @@ -39,6 +40,13 @@ export const createMergePreservesRenamedUpdateTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (state) => state.assertContent("moved.md", "alpha beta extra-update").assertContent("doc.md", "new-content") } + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertContent("moved.md", "alpha beta extra-update") + .assertContent("doc.md", "new-content"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts b/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts index 2b169a1d..dda80042 100644 --- a/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/18-create-rename-create-same-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createRenameCreateSamePathTest: TestDefinition = { @@ -22,7 +23,7 @@ export const createRenameCreateSamePathTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(3) .assertContent("B.md", "first file") diff --git a/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts b/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts index a6c6851b..fe9267d4 100644 --- a/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts +++ b/frontend/deterministic-tests/src/tests/19-move-chain-three-files.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveChainThreeFilesTest: TestDefinition = { @@ -29,7 +30,7 @@ export const moveChainThreeFilesTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(3) .assertContent("A.md", "was C") diff --git a/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts index 0616136b..467c19f0 100644 --- a/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts +++ b/frontend/deterministic-tests/src/tests/2-binary-pending-create-not-displaced.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const binaryPendingCreateNotDisplacedTest: TestDefinition = { @@ -23,6 +24,17 @@ export const binaryPendingCreateNotDisplacedTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(2).assertFileExists("data.bin").assertFileExists("data (1).bin").assertAnyFileContains("binary data from client 0", "binary data from client 1") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(2) + .assertFileExists("data.bin") + .assertFileExists("data (1).bin") + .assertAnyFileContains( + "binary data from client 0", + "binary data from client 1" + ); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts index 33fb8107..69a5ff10 100644 --- a/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts +++ b/frontend/deterministic-tests/src/tests/3-coalesce-update-remote-update-data-loss.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { @@ -38,10 +39,14 @@ export const coalesceUpdateRemoteUpdateDataLossTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(1) - .assertContains("doc.md", "client 0 addition", "client 1 addition"); + .assertContains( + "doc.md", + "client 0 addition", + "client 1 addition" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts index 15fe3e82..8b1cd242 100644 --- a/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts +++ b/frontend/deterministic-tests/src/tests/4-coalesced-remote-update-watermark-loss.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { @@ -18,7 +19,12 @@ export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { { type: "sync", client: 0 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "final update"); + } + }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, @@ -26,13 +32,23 @@ export const coalescedRemoteUpdateWatermarkLossTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "final update"); + } + }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "final update") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "final update"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts index 3108ecfe..88376f22 100644 --- a/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/5-concurrent-delete-during-remote-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = { @@ -21,7 +22,11 @@ export const concurrentDeleteDuringRemoteUpdateTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (state) => state.assertFileCount(0) } + { + type: "assert-consistent", + verify: (state: AssertableState): void => { + state.assertFileCount(0); + } + } ] }; - diff --git a/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts index 08778488..5c141a0e 100644 --- a/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts +++ b/frontend/deterministic-tests/src/tests/6-concurrent-edit-exact-same-position.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentEditExactSamePositionTest: TestDefinition = { @@ -38,7 +39,7 @@ export const concurrentEditExactSamePositionTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(1) .assertContains("doc.md", "slow", "fast", "brown fox"); diff --git a/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts index 3e71ed7d..c69e391c 100644 --- a/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts +++ b/frontend/deterministic-tests/src/tests/7-concurrent-rename-and-create-at-target.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { @@ -37,10 +38,14 @@ export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileNotExists("X.md") - .assertContains("Y.md", "original file X", "brand new Y content"); + .assertContains( + "Y.md", + "original file X", + "brand new Y content" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts index 9f0b0318..a6f34102 100644 --- a/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts +++ b/frontend/deterministic-tests/src/tests/8-concurrent-rename-and-create-at-target.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { @@ -37,7 +38,7 @@ export const concurrentRenameAndCreateAtTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(2) .assertContains("Y (1).md", "original file X") diff --git a/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts index 230c7a1d..eff10952 100644 --- a/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/9-concurrent-rename-same-target.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameSameTargetTest: TestDefinition = { @@ -25,7 +26,7 @@ export const concurrentRenameSameTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(2) .assertFileNotExists("A.md") diff --git a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts index f6e14152..8b934c1b 100644 --- a/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts +++ b/frontend/deterministic-tests/src/tests/binary-to-text-transition.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const binaryToTextTransitionTest: TestDefinition = { @@ -8,11 +9,21 @@ export const binaryToTextTransitionTest: TestDefinition = { "offline. The text merge should preserve both edits.", clients: 2, steps: [ - { type: "create", client: 0, path: "data.bin", content: "original content" }, + { + type: "create", + client: 0, + path: "data.bin", + content: "original content" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("data.bin", "original content") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("data.bin", "original content"); + } + }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, @@ -24,26 +35,63 @@ export const binaryToTextTransitionTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContainsAny("data.bin", "version A", "version B") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContainsAny( + "data.bin", + "version A", + "version B" + ); + } + }, { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "data.bin", newPath: "data.md" }, - { type: "update", client: 0, path: "data.md", content: "top line\nmiddle line\nbottom line" }, + { + type: "update", + client: 0, + path: "data.md", + content: "top line\nmiddle line\nbottom line" + }, { type: "sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("data.md", "top line\nmiddle line\nbottom line") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent( + "data.md", + "top line\nmiddle line\nbottom line" + ); + } + }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, - { type: "update", client: 0, path: "data.md", content: "alpha\nmiddle line\nbottom line" }, - { type: "update", client: 1, path: "data.md", content: "top line\nmiddle line\nbeta" }, + { + type: "update", + client: 0, + path: "data.md", + content: "alpha\nmiddle line\nbottom line" + }, + { + type: "update", + client: 1, + path: "data.md", + content: "top line\nmiddle line\nbeta" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("data.md", "alpha", "beta") }, - ], + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains("data.md", "alpha", "beta"); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts index 1dddcf7a..aef7688d 100644 --- a/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts +++ b/frontend/deterministic-tests/src/tests/concurrent-rename-first-wins.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const concurrentRenameFirstWinsTest: TestDefinition = { @@ -8,29 +9,52 @@ export const concurrentRenameFirstWinsTest: TestDefinition = { "edits are merged.", clients: 2, steps: [ - { type: "create", client: 0, path: "A.md", content: "line 1\nline 2\nline 3" }, + { + type: "create", + client: 0, + path: "A.md", + content: "line 1\nline 2\nline 3" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "line 1\nline 2\nline 3") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("A.md", "line 1\nline 2\nline 3"); + } + }, { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, - { type: "update", client: 0, path: "B.md", content: "edit from 0\nline 2\nline 3" }, + { + type: "update", + client: 0, + path: "B.md", + content: "edit from 0\nline 2\nline 3" + }, { type: "rename", client: 1, oldPath: "A.md", newPath: "C.md" }, - { type: "update", client: 1, path: "C.md", content: "line 1\nline 2\nedit from 1" }, + { + type: "update", + client: 1, + path: "C.md", + content: "line 1\nline 2\nedit from 1" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => { - s.assertFileNotExists("A.md"); - s.assertFileCount(1); - s.assertAnyFileContains("edit from 0", "edit from 1"); - } }, - ], + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md"); + s.assertFileCount(1); + s.assertAnyFileContains("edit from 0", "edit from 1"); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts index 5bec2bcb..20d9e621 100644 --- a/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts +++ b/frontend/deterministic-tests/src/tests/create-rename-response-skips-file.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const createRenameResponseSkipsFileTest: TestDefinition = { @@ -29,6 +30,11 @@ export const createRenameResponseSkipsFileTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertAnyFileContains("the-content") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertAnyFileContains("the-content"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts index 204e9896..dfef9961 100644 --- a/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-by-other-client-then-recreate.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteByOtherClientThenRecreateTest: TestDefinition = { @@ -14,11 +15,26 @@ export const deleteByOtherClientThenRecreateTest: TestDefinition = { { type: "delete", client: 1, path: "A.md" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileNotExists("A.md") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md"); + } + }, - { type: "create", client: 0, path: "A.md", content: "recreated by client 0" }, + { + type: "create", + client: 0, + path: "A.md", + content: "recreated by client 0" + }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "recreated by client 0") }, - ], + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("A.md", "recreated by client 0"); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts index f6236060..831c2f05 100644 --- a/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-during-pending-create.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteDuringPendingCreateTest: TestDefinition = { @@ -26,6 +27,11 @@ export const deleteDuringPendingCreateTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("ephemeral.md") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(0).assertFileNotExists("ephemeral.md"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts index c95c6aa4..0d4bcffb 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-concurrent-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateConcurrentUpdateTest: TestDefinition = { @@ -14,7 +15,12 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = { { type: "disable-sync", client: 0 }, { type: "delete", client: 0, path: "A.md" }, - { type: "create", client: 0, path: "A.md", content: "recreated by client 0" }, + { + type: "create", + client: 0, + path: "A.md", + content: "recreated by client 0" + }, { type: "update", @@ -28,6 +34,11 @@ export const deleteRecreateConcurrentUpdateTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileExists("A.md").assertContains("A.md", "recreated") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileExists("A.md").assertContains("A.md", "recreated"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts index 02197b8d..7ecd21a3 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-different-content.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateDifferentContentTest: TestDefinition = { @@ -41,6 +42,15 @@ export const deleteRecreateDifferentContentTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("A.md", "brand new", "client 1") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "A.md", + "brand new", + "client 1" + ); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts b/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts index 10b00f70..4b2a836b 100644 --- a/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-recreate-same-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRecreateSamePathTest: TestDefinition = { @@ -11,7 +12,12 @@ export const deleteRecreateSamePathTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "version 1") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("A.md", "version 1"); + } + }, { type: "disable-sync", client: 0 }, { type: "delete", client: 0, path: "A.md" }, @@ -20,6 +26,11 @@ export const deleteRecreateSamePathTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("A.md", "version 2") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("A.md", "version 2"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts index 4cbeed25..7eeb80ad 100644 --- a/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/delete-rename-conflict.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const deleteRenameConflictTest: TestDefinition = { @@ -12,7 +13,12 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileExists("A.md").assertFileExists("B.md") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileExists("A.md").assertFileExists("B.md"); + } + }, { type: "disable-sync", client: 1 }, @@ -25,10 +31,15 @@ export const deleteRenameConflictTest: TestDefinition = { { type: "sync", client: 1 }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => { - s.assertContent("B.md", "content-b"); - s.assertFileNotExists("A.md"); - s.ifFileExists("C.md", (s) => s.assertContent("C.md", "content-a")); - } }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("B.md", "content-b"); + s.assertFileNotExists("A.md"); + s.ifFileExists("C.md", (inner) => + inner.assertContent("C.md", "content-a") + ); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts b/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts index 99d5f716..326343af 100644 --- a/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts +++ b/frontend/deterministic-tests/src/tests/displaced-file-not-marked-deleted.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const displacedFileNotMarkedDeletedTest: TestDefinition = { @@ -20,14 +21,19 @@ export const displacedFileNotMarkedDeletedTest: TestDefinition = { { type: "sync", client: 0 }, { type: "rename", client: 1, oldPath: "A.md", newPath: "B.md" }, - { type: "update", client: 1, path: "B.md", content: "edited A content" }, + { + type: "update", + client: 1, + path: "B.md", + content: "edited A content" + }, { type: "enable-sync", client: 1 }, { type: "barrier" }, { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileNotExists("A.md") .assertFileExists("B.md") diff --git a/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts b/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts index 1034ce27..f617ca5f 100644 --- a/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/double-offline-cycle.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const doubleOfflineCycleTest: TestDefinition = { @@ -16,7 +17,12 @@ export const doubleOfflineCycleTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "initial") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("doc.md", "initial"); + } + }, { type: "disable-sync", client: 0 }, { @@ -29,7 +35,12 @@ export const doubleOfflineCycleTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "first edit") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("doc.md", "first edit"); + } + }, { type: "disable-sync", client: 0 }, { @@ -42,7 +53,12 @@ export const doubleOfflineCycleTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertContent("doc.md", "second edit") }, + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertContent("doc.md", "second edit"); + } + }, { type: "disable-sync", client: 0 }, { @@ -55,6 +71,11 @@ export const doubleOfflineCycleTest: TestDefinition = { { type: "enable-sync", client: 0 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "third edit") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "third edit"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts b/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts index f9ae2a3f..b0512617 100644 --- a/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts +++ b/frontend/deterministic-tests/src/tests/failed-vfs-move-falls-back.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const failedVfsMoveFallsBackTest: TestDefinition = { @@ -17,6 +18,11 @@ export const failedVfsMoveFallsBackTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("B.md", "content A") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("B.md", "content A"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts index ce12df0c..58c57511 100644 --- a/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts +++ b/frontend/deterministic-tests/src/tests/idempotency-after-server-pause.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const idempotencyAfterServerPauseTest: TestDefinition = { @@ -11,7 +12,12 @@ export const idempotencyAfterServerPauseTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "create", client: 0, path: "doc.md", content: "important data" }, + { + type: "create", + client: 0, + path: "doc.md", + content: "important data" + }, { type: "pause-server" }, { type: "resume-server" }, @@ -19,6 +25,11 @@ export const idempotencyAfterServerPauseTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("doc.md", "important data") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "important data"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts index ef8404fb..444adc56 100644 --- a/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts +++ b/frontend/deterministic-tests/src/tests/interrupted-delete-retry.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const interruptedDeleteRetryTest: TestDefinition = { @@ -20,6 +21,11 @@ export const interruptedDeleteRetryTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(0) }, - ], + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(0); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts b/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts index 9d9a870d..f29fa45b 100644 --- a/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts +++ b/frontend/deterministic-tests/src/tests/key-migration-event-drop.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const keyMigrationEventDropTest: TestDefinition = { @@ -30,6 +31,11 @@ export const keyMigrationEventDropTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContent("A.md", "updated content") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("A.md", "updated content"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts index 66c832db..20925889 100644 --- a/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/local-edit-lost-during-create-merge.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const localEditLostDuringCreateMergeTest: TestDefinition = { @@ -28,12 +29,13 @@ export const localEditLostDuringCreateMergeTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => + verify: (s: AssertableState): void => { s.assertFileCount(1).assertContains( "doc.md", "from-client-1", "local-edit-during-create" - ), + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts index ce991df3..b0175b37 100644 --- a/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-cross-create-rename-same-target.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcCrossCreateRenameSameTargetTest: TestDefinition = { @@ -17,7 +18,9 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileExists("X.md").assertFileExists("Y.md") + verify: (s: AssertableState): void => { + s.assertFileExists("X.md").assertFileExists("Y.md"); + } }, { type: "disable-sync", client: 1 }, @@ -33,7 +36,7 @@ export const mcCrossCreateRenameSameTargetTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertFileCount(2) .assertFileNotExists("X.md") .assertFileNotExists("Y.md") diff --git a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts index 98504f03..0808c65a 100644 --- a/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-delete-then-offline-rename.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcDeleteThenOfflineRenameTest: TestDefinition = { @@ -27,10 +28,13 @@ export const mcDeleteThenOfflineRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { - s.assertContent("C.md", "unrelated") - .assertFileNotExists("A.md"); - s.ifFileExists("B.md", (s) => s.assertContent("B.md", "original")); + verify: (s: AssertableState): void => { + s.assertContent("C.md", "unrelated").assertFileNotExists( + "A.md" + ); + s.ifFileExists("B.md", (inner) => + inner.assertContent("B.md", "original") + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts index 26a095d5..1dbb3464 100644 --- a/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-multi-delete-offline-rename.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcMultiDeleteOfflineRenameTest: TestDefinition = { @@ -22,7 +23,12 @@ export const mcMultiDeleteOfflineRenameTest: TestDefinition = { { type: "delete", client: 1, path: "file-4.md" }, { type: "sync", client: 1 }, - { type: "rename", client: 0, oldPath: "file-2.md", newPath: "renamed.md" }, + { + type: "rename", + client: 0, + oldPath: "file-2.md", + newPath: "renamed.md" + }, { type: "enable-sync", client: 0 }, { type: "sync" }, @@ -30,13 +36,15 @@ export const mcMultiDeleteOfflineRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertFileExists("file-1.md") .assertFileExists("file-3.md") .assertFileExists("file-5.md") .assertFileNotExists("file-2.md") .assertFileNotExists("file-4.md"); - s.ifFileExists("renamed.md", (s) => s.assertContent("renamed.md", "content-2")); + s.ifFileExists("renamed.md", (inner) => + inner.assertContent("renamed.md", "content-2") + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts index 8144bbb5..3ab451e2 100644 --- a/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts +++ b/frontend/deterministic-tests/src/tests/mc-three-client-rename-offline-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { @@ -19,12 +20,24 @@ export const mcThreeClientRenameOfflineUpdateTest: TestDefinition = { { type: "sync", client: 1 }, { type: "sync", client: 0 }, - { type: "update", client: 2, path: "A.md", content: "updated-by-client-2" }, + { + type: "update", + client: 2, + path: "A.md", + content: "updated-by-client-2" + }, { type: "enable-sync", client: 2 }, { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileNotExists("A.md").assertContains("B.md", "updated-by-client-2") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1) + .assertFileNotExists("A.md") + .assertContains("B.md", "updated-by-client-2"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts b/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts index a4f6d3d3..a230df24 100644 --- a/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts +++ b/frontend/deterministic-tests/src/tests/migrate-key-preserves-existing.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const migrateKeyPreservesExistingTest: TestDefinition = { @@ -25,6 +26,14 @@ export const migrateKeyPreservesExistingTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertContains("A.md", "updated by client 0") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "A.md", + "updated by client 0" + ); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts index f590f5b4..c1453390 100644 --- a/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-and-concurrent-remote-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { @@ -32,6 +33,13 @@ export const moveAndConcurrentRemoteUpdateTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(1).assertFileNotExists("A.md").assertContains("B.md", "updated by client 1") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(1) + .assertFileNotExists("A.md") + .assertContains("B.md", "updated by client 1"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts index 59bedbbe..aae5f18c 100644 --- a/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/move-preserves-remote-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const movePreservesRemoteUpdateTest: TestDefinition = { @@ -6,7 +7,12 @@ export const movePreservesRemoteUpdateTest: TestDefinition = { "After both reconnect, the renamed file should contain client 1's edit.", clients: 2, steps: [ - { type: "create", client: 0, path: "doc.md", content: "line 1\nline 2" }, + { + type: "create", + client: 0, + path: "doc.md", + content: "line 1\nline 2" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "sync" }, @@ -16,7 +22,12 @@ export const movePreservesRemoteUpdateTest: TestDefinition = { { type: "disable-sync", client: 1 }, { type: "rename", client: 0, oldPath: "doc.md", newPath: "renamed.md" }, - { type: "update", client: 1, path: "doc.md", content: "line 1\nclient 1 edit\nline 2" }, + { + type: "update", + client: 1, + path: "doc.md", + content: "line 1\nclient 1 edit\nline 2" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, @@ -25,13 +36,15 @@ export const movePreservesRemoteUpdateTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertFileCount(1); - const content = Array.from(s.files.values())[0]; + const [content] = Array.from(s.files.values()); if (!content.includes("client 1 edit")) { - throw new Error(`Expected merged content to include "client 1 edit", got: "${content}"`); + throw new Error( + `Expected merged content to include "client 1 edit", got: "${content}"` + ); } } - }, - ], + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts index 95fcfe26..29e3fd27 100644 --- a/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/move-remote-update-reverts-rename.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { @@ -13,7 +14,12 @@ export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { { type: "barrier" }, { type: "disable-sync", client: 0 }, - { type: "update", client: 1, path: "doc.md", content: "updated by client 1" }, + { + type: "update", + client: 1, + path: "doc.md", + content: "updated by client 1" + }, { type: "sync", client: 1 }, { type: "enable-sync", client: 0 }, @@ -23,11 +29,13 @@ export const moveRemoteUpdateRevertsRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertFileCount(1); - const content = Array.from(s.files.values())[0]; + const [content] = Array.from(s.files.values()); if (content !== "updated by client 1") { - throw new Error(`Expected "updated by client 1", got: "${content}"`); + throw new Error( + `Expected "updated by client 1", got: "${content}"` + ); } } } diff --git a/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts b/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts index 77814669..dbbec7af 100644 --- a/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts +++ b/frontend/deterministic-tests/src/tests/move-then-delete-stale-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const moveThenDeleteStalePathTest: TestDefinition = { @@ -23,6 +24,13 @@ export const moveThenDeleteStalePathTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(0).assertFileNotExists("A.md").assertFileNotExists("B.md") } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(0) + .assertFileNotExists("A.md") + .assertFileNotExists("B.md"); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts index 66efd778..b241433d 100644 --- a/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts +++ b/frontend/deterministic-tests/src/tests/multi-file-operations.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const multiFileOperationsTest: TestDefinition = { @@ -19,7 +20,12 @@ export const multiFileOperationsTest: TestDefinition = { { type: "delete", client: 0, path: "A.md" }, { type: "sync", client: 0 }, - { type: "update", client: 1, path: "B.md", content: "updated by client 1" }, + { + type: "update", + client: 1, + path: "B.md", + content: "updated by client 1" + }, { type: "rename", client: 1, oldPath: "A.md", newPath: "D.md" }, { type: "enable-sync", client: 1 }, @@ -28,11 +34,13 @@ export const multiFileOperationsTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertContains("B.md", "updated") .assertFileExists("C.md") .assertFileNotExists("A.md"); - s.ifFileExists("D.md", (s) => s.assertContent("D.md", "content-a")); + s.ifFileExists("D.md", (inner) => + inner.assertContent("D.md", "content-a") + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts index 56ecc00d..ff16608b 100644 --- a/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-concurrent-renames.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineConcurrentRenamesTest: TestDefinition = { @@ -15,7 +16,9 @@ export const offlineConcurrentRenamesTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "shared-content") + verify: (s: AssertableState): void => { + s.assertContent("A.md", "shared-content"); + } }, { type: "disable-sync", client: 0 }, @@ -42,15 +45,15 @@ export const offlineConcurrentRenamesTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { s.assertFileNotExists("A.md") .assertFileCount(1) .assertAnyFileContains("shared-content"); - s.ifFileExists("B.md", (s) => - s.assertContent("B.md", "shared-content") + s.ifFileExists("B.md", (inner) => + inner.assertContent("B.md", "shared-content") ); - s.ifFileExists("C.md", (s) => - s.assertContent("C.md", "shared-content") + s.ifFileExists("C.md", (inner) => + inner.assertContent("C.md", "shared-content") ); } } diff --git a/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts b/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts index ca777563..9a4939ef 100644 --- a/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-create-same-path-binary-conflict.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineCreateSamePathMergeableTest: TestDefinition = { @@ -27,15 +28,15 @@ export const offlineCreateSamePathMergeableTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(1) + verify: (s: AssertableState): void => { + s.assertFileCount(1) .assertFileExists("notes.md") .assertContains( "notes.md", "alpha wrote this line", "beta wrote this different line" - ) + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts index ed242b20..1e9ea8f7 100644 --- a/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-delete-remote-rename.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineDeleteRemoteRenameTest: TestDefinition = { @@ -27,9 +28,10 @@ export const offlineDeleteRemoteRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { - s.assertFileNotExists("A.md") - .assertFileNotExists("A_renamed.md"); + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md").assertFileNotExists( + "A_renamed.md" + ); } } ] diff --git a/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts b/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts index d86e3066..73db9efa 100644 --- a/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-delete-vs-remote-update.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { @@ -17,7 +18,9 @@ export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original content") + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original content"); + } }, { type: "disable-sync", client: 0 }, @@ -37,7 +40,9 @@ export const offlineDeleteVsRemoteUpdateTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileCount(0) + verify: (s: AssertableState): void => { + s.assertFileCount(0); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts index fc4383e4..0d6c0be5 100644 --- a/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-edit-remote-rename.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineEditRemoteRenameTest: TestDefinition = { @@ -13,7 +14,9 @@ export const offlineEditRemoteRenameTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original") + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original"); + } }, { type: "disable-sync", client: 0 }, @@ -38,11 +41,11 @@ export const offlineEditRemoteRenameTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("A.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md") .assertFileCount(1) - .assertContains("B.md", "edited by client 0") + .assertContains("B.md", "edited by client 0"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts b/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts index 77d50099..074874a8 100644 --- a/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-edit-then-move-same-content.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineEditThenMoveSameContentTest: TestDefinition = { @@ -41,12 +42,12 @@ export const offlineEditThenMoveSameContentTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("A.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md") .assertFileNotExists("B.md") .assertContent("C.md", "content A") - .assertFileCount(1) + .assertFileCount(1); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts b/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts index 68453a0e..06f890d1 100644 --- a/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-mixed-operations.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMixedOperationsTest: TestDefinition = { @@ -17,11 +18,11 @@ export const offlineMixedOperationsTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertContent("file1.md", "content-1") + verify: (s: AssertableState): void => { + s.assertContent("file1.md", "content-1") .assertContent("file2.md", "content-2") - .assertContent("file3.md", "content-3") + .assertContent("file3.md", "content-3"); + } }, { type: "disable-sync", client: 0 }, @@ -46,13 +47,13 @@ export const offlineMixedOperationsTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("file1.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("file1.md") .assertFileNotExists("file2.md") .assertContent("moved.md", "content-2") .assertContent("file3.md", "updated-content-3") - .assertFileCount(2) + .assertFileCount(2); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts b/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts index d1522528..1ded0e6e 100644 --- a/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-move-then-remote-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMoveThenRemoteDeleteTest: TestDefinition = { @@ -29,11 +30,11 @@ export const offlineMoveThenRemoteDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("A.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md") .assertFileNotExists("B.md") - .assertFileCount(0) + .assertFileCount(0); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts b/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts index e242223a..08aed64d 100644 --- a/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-multiple-edits.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineMultipleEditsTest: TestDefinition = { @@ -14,7 +15,9 @@ export const offlineMultipleEditsTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("doc.md", "original") + verify: (s: AssertableState): void => { + s.assertContent("doc.md", "original"); + } }, { type: "disable-sync", client: 0 }, @@ -31,8 +34,9 @@ export const offlineMultipleEditsTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("doc.md", "edit-5-final") + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "edit-5-final"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts b/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts index c446d459..0cc02c88 100644 --- a/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-rename-and-edit.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineRenameAndEditTest: TestDefinition = { @@ -14,12 +15,19 @@ export const offlineRenameAndEditTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original") + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original"); + } }, { type: "disable-sync", client: 0 }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, - { type: "update", client: 0, path: "B.md", content: "edited after rename" }, + { + type: "update", + client: 0, + path: "B.md", + content: "edited after rename" + }, { type: "enable-sync", client: 0 }, { type: "sync" }, @@ -27,11 +35,11 @@ export const offlineRenameAndEditTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("A.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md") .assertFileCount(1) - .assertContent("B.md", "edited after rename") + .assertContent("B.md", "edited after rename"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts index 24f4ff2a..b20061f6 100644 --- a/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-rename-remote-create-old-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { @@ -14,7 +15,9 @@ export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("X.md", "original") + verify: (s: AssertableState): void => { + s.assertContent("X.md", "original"); + } }, { type: "disable-sync", client: 0 }, @@ -39,10 +42,12 @@ export const offlineRenameRemoteCreateOldPathTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(1) - .assertContains("Y.md", "updated-by-client-1") + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "Y.md", + "updated-by-client-1" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts b/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts index 47a88328..3019f2ae 100644 --- a/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts +++ b/frontend/deterministic-tests/src/tests/offline-update-both-then-delete-one.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { @@ -26,10 +27,12 @@ export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => - s - .assertContent("A.md", "A original") - .assertContent("B.md", "B original") + verify: (s: AssertableState): void => { + s.assertContent("A.md", "A original").assertContent( + "B.md", + "B original" + ); + } }, { type: "disable-sync", client: 0 }, @@ -63,10 +66,12 @@ export const offlineUpdateBothThenDeleteOneTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertContent("A.md", "A updated by client 0") - .assertFileNotExists("B.md") + verify: (s: AssertableState): void => { + s.assertContent( + "A.md", + "A updated by client 0" + ).assertFileNotExists("B.md"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts index 1639ed90..b951b0be 100644 --- a/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts +++ b/frontend/deterministic-tests/src/tests/online-both-create-same-path-deconflict.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { @@ -23,7 +24,7 @@ export const onlineBothCreateSamePathDeconflictTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(1) .assertContains("A.md", "updated-by-0", "from-client-1 "); diff --git a/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts b/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts index 3449e676..f86b3347 100644 --- a/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts +++ b/frontend/deterministic-tests/src/tests/online-create-rename-concurrent-create-orphan.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { @@ -12,8 +13,18 @@ export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { { type: "disable-sync", client: 0 }, - { type: "create", client: 0, path: "data.bin", content: "BINARY:offline-content" }, - { type: "rename", client: 0, oldPath: "data.bin", newPath: "moved.bin" }, + { + type: "create", + client: 0, + path: "data.bin", + content: "BINARY:offline-content" + }, + { + type: "rename", + client: 0, + oldPath: "data.bin", + newPath: "moved.bin" + }, { type: "enable-sync", client: 0 }, { type: "delete", client: 0, path: "moved.bin" }, @@ -22,7 +33,7 @@ export const onlineCreateRenameConcurrentCreateOrphanTest: TestDefinition = { { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state.assertFileCount(0); } } diff --git a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts index f59a92e3..68a64e9f 100644 --- a/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/online-create-update-while-other-creates-same-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { @@ -11,16 +12,33 @@ export const onlineCreateUpdateWhileOtherCreatesSamePathTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "pause-websocket", client: 1 }, - { type: "create", client: 0, path: "data.bin", content: "BINARY:content-v1" }, - { type: "update", client: 0, path: "data.bin", content: "BINARY:content-v2" }, - { type: "create", client: 1, path: "data.bin", content: "BINARY:other-content" }, + { + type: "create", + client: 0, + path: "data.bin", + content: "BINARY:content-v1" + }, + { + type: "update", + client: 0, + path: "data.bin", + content: "BINARY:content-v2" + }, + { + type: "create", + client: 1, + path: "data.bin", + content: "BINARY:other-content" + }, { type: "resume-websocket", client: 1 }, { type: "barrier" }, { - type: "assert-consistent", verify: (state) => { - state.assertFileCount(2) + type: "assert-consistent", + verify: (state: AssertableState): void => { + state + .assertFileCount(2) .assertContains("data.bin", "content-v2") .assertContains("data (1).bin", "other-content"); } diff --git a/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts b/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts index b575aa58..de5d6c89 100644 --- a/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/online-delete-recreate-rapid-cycle.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineDeleteRecreateRapidCycleTest: TestDefinition = { @@ -28,7 +29,9 @@ export const onlineDeleteRecreateRapidCycleTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "round 3"), - }, - ], + verify: (s: AssertableState): void => { + s.assertContent("A.md", "round 3"); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts b/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts index 16ed7236..41a9d871 100644 --- a/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts +++ b/frontend/deterministic-tests/src/tests/online-edit-vs-delete-convergence.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const onlineEditVsDeleteConvergenceTest: TestDefinition = { @@ -11,17 +12,22 @@ export const onlineEditVsDeleteConvergenceTest: TestDefinition = { { type: "enable-sync", client: 1 }, { type: "barrier" }, - { type: "update", client: 0, path: "A.md", content: "edited by client 0" }, + { + type: "update", + client: 0, + path: "A.md", + content: "edited by client 0" + }, { type: "delete", client: 1, path: "A.md" }, { type: "barrier" }, { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state.ifFileExists("A.md", (s) => s.assertContainsAny("A.md", "edited by client 0") ); } - }, - ], + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts index eeb705de..14b013d6 100644 --- a/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts +++ b/frontend/deterministic-tests/src/tests/overlapping-edits-same-section.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const overlappingEditsSameSectionTest: TestDefinition = { @@ -41,9 +42,15 @@ export const overlappingEditsSameSectionTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1) - .assertContains("doc.md", "# Title", "alpha addition", "beta addition", "footer"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "doc.md", + "# Title", + "alpha addition", + "beta addition", + "footer" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts index ecf58d05..6d89acf4 100644 --- a/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts +++ b/frontend/deterministic-tests/src/tests/queue-reset-loses-coalesced-local-edit.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const queueResetLosesCoalescedLocalEditTest: TestDefinition = { @@ -23,8 +24,13 @@ export const queueResetLosesCoalescedLocalEditTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContains("doc.md", "alpha", "charlie"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "doc.md", + "alpha", + "charlie" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts index 45f90144..db9ed848 100644 --- a/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-create-update-delete-cycle.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { @@ -41,7 +42,12 @@ export const rapidCreateUpdateDeleteCycleTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileCount(1).assertContent("cycle.md", "final creation"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent( + "cycle.md", + "final creation" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts index 042942b3..48c062e0 100644 --- a/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-edit-delete-online-convergence.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { @@ -28,17 +29,20 @@ export const rapidEditDeleteOnlineConvergenceTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => { + verify: (s: AssertableState): void => { for (const [path, content] of s.files) { for (const clientFiles of s.clientFiles) { - if (clientFiles.has(path) && clientFiles.get(path) !== content) { + if ( + clientFiles.has(path) && + clientFiles.get(path) !== content + ) { throw new Error( `Content mismatch for ${path}: "${clientFiles.get(path)}" vs "${content}"` ); } } } - }, - }, - ], + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts index bf0ed488..1a155814 100644 --- a/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts +++ b/frontend/deterministic-tests/src/tests/rapid-updates-after-merge.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const rapidUpdatesAfterMergeTest: TestDefinition = { @@ -42,7 +43,9 @@ export const rapidUpdatesAfterMergeTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileCount(1).assertContains("doc.md", "update 3"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains("doc.md", "update 3"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts b/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts index 128cd90e..c8e70243 100644 --- a/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts +++ b/frontend/deterministic-tests/src/tests/recently-deleted-cleared-on-reconnect.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { @@ -19,7 +20,12 @@ export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { { type: "disable-sync", client: 0 }, { type: "disable-sync", client: 1 }, - { type: "create", client: 1, path: "doc.md", content: "new content from client 1" }, + { + type: "create", + client: 1, + path: "doc.md", + content: "new content from client 1" + }, { type: "enable-sync", client: 1 }, { type: "sync", client: 1 }, @@ -28,8 +34,12 @@ export const recentlyDeletedClearedOnReconnectTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("doc.md", "new content from client 1"), - }, - ], + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent( + "doc.md", + "new content from client 1" + ); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts index 27787e4f..97661f4f 100644 --- a/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-chain-then-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameChainThenDeleteTest: TestDefinition = { @@ -13,7 +14,9 @@ export const renameChainThenDeleteTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("X.md", "chain-content"), + verify: (s: AssertableState): void => { + s.assertContent("X.md", "chain-content"); + } }, { type: "disable-sync", client: 1 }, @@ -39,6 +42,11 @@ export const renameChainThenDeleteTest: TestDefinition = { { type: "sync" }, { type: "barrier" }, - { type: "assert-consistent", verify: (s) => s.assertFileCount(0) } + { + type: "assert-consistent", + verify: (s: AssertableState): void => { + s.assertFileCount(0); + } + } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-chain.test.ts b/frontend/deterministic-tests/src/tests/rename-chain.test.ts index 8cc3bde3..15365fc1 100644 --- a/frontend/deterministic-tests/src/tests/rename-chain.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-chain.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameChainTest: TestDefinition = { @@ -9,7 +10,12 @@ export const renameChainTest: TestDefinition = { steps: [ { type: "enable-sync", client: 1 }, - { type: "create", client: 0, path: "A.md", content: "important content" }, + { + type: "create", + client: 0, + path: "A.md", + content: "important content" + }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, { type: "rename", client: 0, oldPath: "B.md", newPath: "C.md" }, @@ -19,10 +25,11 @@ export const renameChainTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => + verify: (s: AssertableState): void => { s.assertFileNotExists("A.md") .assertFileNotExists("B.md") - .assertContent("C.md", "important content"), + .assertContent("C.md", "important content"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-circular.test.ts b/frontend/deterministic-tests/src/tests/rename-circular.test.ts index 5c85ca71..508182cd 100644 --- a/frontend/deterministic-tests/src/tests/rename-circular.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-circular.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameCircularTest: TestDefinition = { @@ -13,10 +14,11 @@ export const renameCircularTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => + verify: (s: AssertableState): void => { s.assertContent("A.md", "content-a") .assertContent("B.md", "content-b") - .assertContent("C.md", "content-c"), + .assertContent("C.md", "content-c"); + } }, { type: "disable-sync", client: 0 }, @@ -31,12 +33,13 @@ export const renameCircularTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => + verify: (s: AssertableState): void => { s.assertFileNotExists("temp-a.md") .assertFileCount(3) .assertContent("A.md", "content-c") .assertContent("B.md", "content-a") - .assertContent("C.md", "content-b"), + .assertContent("C.md", "content-b"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts index c29b1dc5..635e6e91 100644 --- a/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-create-conflict.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameCreateConflictTest: TestDefinition = { @@ -12,7 +13,9 @@ export const renameCreateConflictTest: TestDefinition = { { type: "sync", client: 1 }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "hi"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "hi"); + } }, { type: "disable-sync", client: 0 }, { type: "rename", client: 1, oldPath: "A.md", newPath: "B.md" }, @@ -23,8 +26,9 @@ export const renameCreateConflictTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("A.md").assertContent("B.md", "hi"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md").assertContent("B.md", "hi"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts index d38a0392..639c51e3 100644 --- a/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-pending-create-before-response.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renamePendingCreateBeforeResponseTest: TestDefinition = { @@ -34,8 +35,12 @@ export const renamePendingCreateBeforeResponseTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("renamed.md", "original-content"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent( + "renamed.md", + "original-content" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts b/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts index bdf043f4..19a1240f 100644 --- a/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-roundtrip.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameRoundtripTest: TestDefinition = { @@ -12,7 +13,9 @@ export const renameRoundtripTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original"); + } }, { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, @@ -21,8 +24,9 @@ export const renameRoundtripTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("A.md").assertContent("B.md", "original"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md").assertContent("B.md", "original"); + } }, { type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" }, @@ -31,8 +35,9 @@ export const renameRoundtripTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("B.md").assertContent("A.md", "original"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("B.md").assertContent("A.md", "original"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-swap.test.ts b/frontend/deterministic-tests/src/tests/rename-swap.test.ts index 18489f33..d531c725 100644 --- a/frontend/deterministic-tests/src/tests/rename-swap.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-swap.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameSwapTest: TestDefinition = { @@ -15,8 +16,12 @@ export const renameSwapTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => - s.assertContent("A.md", "content-a").assertContent("B.md", "content-b"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "content-a").assertContent( + "B.md", + "content-b" + ); + } }, { type: "disable-sync", client: 0 }, @@ -29,12 +34,12 @@ export const renameSwapTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("temp.md") + verify: (s: AssertableState): void => { + s.assertFileNotExists("temp.md") .assertFileCount(2) .assertContent("A.md", "content-b") - .assertContent("B.md", "content-a"), + .assertContent("B.md", "content-a"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts b/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts index b1d09c7f..ddb59e11 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-existing-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToExistingPathTest: TestDefinition = { @@ -19,8 +20,9 @@ export const renameToExistingPathTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("A.md").assertContent("B.md", "alpha"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md").assertContent("B.md", "alpha"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts index b5745e3b..34a3867c 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-path-of-unconfirmed-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToPathOfUnconfirmedDeleteTest: TestDefinition = { @@ -32,10 +33,12 @@ export const renameToPathOfUnconfirmedDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("B.md") - .assertContains("A.md", "content B"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("B.md").assertContains( + "A.md", + "content B" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts index a17f52d4..1d65f9ee 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-pending-path-fallback.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToPendingPathFallbackTest: TestDefinition = { @@ -5,7 +6,12 @@ export const renameToPendingPathFallbackTest: TestDefinition = { "Client 0 creates B.md and syncs. Goes offline, creates A.md, then renames B.md to A.md (overwriting the unsynced A). After reconnecting, B.md should be gone and A.md should have B's content.", clients: 2, steps: [ - { type: "create", client: 0, path: "B.md", content: "tracked B content" }, + { + type: "create", + client: 0, + path: "B.md", + content: "tracked B content" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "sync" }, @@ -13,7 +19,12 @@ export const renameToPendingPathFallbackTest: TestDefinition = { { type: "disable-sync", client: 0 }, - { type: "create", client: 0, path: "A.md", content: "pending A content" }, + { + type: "create", + client: 0, + path: "A.md", + content: "pending A content" + }, { type: "rename", client: 0, oldPath: "B.md", newPath: "A.md" }, @@ -23,8 +34,12 @@ export const renameToPendingPathFallbackTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("B.md").assertContains("A.md", "tracked B content"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("B.md").assertContains( + "A.md", + "tracked B content" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts b/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts index 754c0c18..a7a8a9a5 100644 --- a/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-to-recently-deleted-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameToRecentlyDeletedPathTest: TestDefinition = { @@ -30,11 +31,11 @@ export const renameToRecentlyDeletedPathTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(1) + verify: (s: AssertableState): void => { + s.assertFileCount(1) .assertFileNotExists("A.md") - .assertContent("B.md", "content-a"), + .assertContent("B.md", "content-a"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts index 099009fb..27cae589 100644 --- a/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts +++ b/frontend/deterministic-tests/src/tests/rename-update-conflict.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const renameUpdateConflictTest: TestDefinition = { @@ -12,7 +13,9 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original"); + } }, { type: "disable-sync", client: 1 }, @@ -20,7 +23,12 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "rename", client: 0, oldPath: "A.md", newPath: "B.md" }, { type: "sync", client: 0 }, - { type: "update", client: 1, path: "A.md", content: "updated by client 1" }, + { + type: "update", + client: 1, + path: "A.md", + content: "updated by client 1" + }, { type: "enable-sync", client: 1 }, { type: "sync", client: 1 }, @@ -28,8 +36,9 @@ export const renameUpdateConflictTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileNotExists("A.md").assertContains("B.md", "updated"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("A.md").assertContains("B.md", "updated"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts index e7b001e2..5b14256a 100644 --- a/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts +++ b/frontend/deterministic-tests/src/tests/reset-clears-recently-deleted-resurrection.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { @@ -26,7 +27,9 @@ export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileNotExists("ghost.md"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("ghost.md"); + } }, { type: "disable-sync", client: 1 }, @@ -36,7 +39,9 @@ export const resetClearsRecentlyDeletedResurrectionTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileCount(0), + verify: (s: AssertableState): void => { + s.assertFileCount(0); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts b/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts index 968166a9..0169fbba 100644 --- a/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts +++ b/frontend/deterministic-tests/src/tests/sequential-create-duplicate-content.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const sequentialCreateDuplicateContentTest: TestDefinition = { @@ -5,7 +6,12 @@ export const sequentialCreateDuplicateContentTest: TestDefinition = { "Client 0 creates A.md, syncs, then creates B.md with identical content. Both files must remain as separate documents on both clients.", clients: 2, steps: [ - { type: "create", client: 0, path: "A.md", content: "identical content here" }, + { + type: "create", + client: 0, + path: "A.md", + content: "identical content here" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, { type: "sync" }, @@ -13,20 +19,27 @@ export const sequentialCreateDuplicateContentTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "identical content here"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "identical content here"); + } }, - { type: "create", client: 0, path: "B.md", content: "identical content here" }, + { + type: "create", + client: 0, + path: "B.md", + content: "identical content here" + }, { type: "sync" }, { type: "barrier" }, { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(2) + verify: (s: AssertableState): void => { + s.assertFileCount(2) .assertContent("A.md", "identical content here") - .assertContent("B.md", "identical content here"), + .assertContent("B.md", "identical content here"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts index fea4adad..359f1a36 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-clients-create.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseBothClientsCreateTest: TestDefinition = { @@ -32,10 +33,12 @@ export const serverPauseBothClientsCreateTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertContains("alpha.md", "from client 0") - .assertContains("beta.md", "from client 1"), + verify: (s: AssertableState): void => { + s.assertContains("alpha.md", "from client 0").assertContains( + "beta.md", + "from client 1" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts index 394a531a..e09c8e6c 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-both-edit-same-file.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseBothEditSameFileTest: TestDefinition = { @@ -39,10 +40,13 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(1) - .assertContains("shared.md", "edited by client 0", "edited by client 1"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "shared.md", + "edited by client 0", + "edited by client 1" + ); + } }, { @@ -56,8 +60,12 @@ export const serverPauseBothEditSameFileTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContains("shared.md", "post-merge edit from client 0"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContains( + "shared.md", + "post-merge edit from client 0" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts b/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts index 920259e1..5ac97f0d 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-delete-recreate.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseDeleteRecreateTest: TestDefinition = { @@ -15,18 +16,23 @@ export const serverPauseDeleteRecreateTest: TestDefinition = { { type: "pause-server" }, - { type: "create", client: 0, path: "A.md", content: "recreated during contention" }, + { + type: "create", + client: 0, + path: "A.md", + content: "recreated during contention" + }, { type: "resume-server" }, { type: "barrier" }, { type: "assert-consistent", - verify: (state) => { + verify: (state: AssertableState): void => { state .assertFileCount(1) .assertContent("A.md", "recreated during contention"); } - }, - ], + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts b/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts index c2d6772e..2f378921 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-rename-edit-resume.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseRenameEditResumeTest: TestDefinition = { @@ -19,7 +20,9 @@ export const serverPauseRenameEditResumeTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("A.md", "original content"), + verify: (s: AssertableState): void => { + s.assertContent("A.md", "original content"); + } }, { type: "pause-server" }, @@ -39,11 +42,11 @@ export const serverPauseRenameEditResumeTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileCount(1) + verify: (s: AssertableState): void => { + s.assertFileCount(1) .assertFileNotExists("A.md") - .assertContent("B.md", "edited after rename during pause"), + .assertContent("B.md", "edited after rename during pause"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts b/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts index 3523cf79..e10e37d9 100644 --- a/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts +++ b/frontend/deterministic-tests/src/tests/server-pause-update-and-create.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const serverPauseUpdateAndCreateTest: TestDefinition = { @@ -17,7 +18,9 @@ export const serverPauseUpdateAndCreateTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => s.assertContent("shared.md", "initial content"), + verify: (s: AssertableState): void => { + s.assertContent("shared.md", "initial content"); + } }, { type: "pause-server" }, @@ -42,10 +45,12 @@ export const serverPauseUpdateAndCreateTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertContent("shared.md", "updated during pause") - .assertContent("new-file.md", "created by client 1"), + verify: (s: AssertableState): void => { + s.assertContent( + "shared.md", + "updated during pause" + ).assertContent("new-file.md", "created by client 1"); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts b/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts index 2e74b3a5..c7f71165 100644 --- a/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts +++ b/frontend/deterministic-tests/src/tests/simultaneous-create-delete-same-path.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const simultaneousCreateDeleteSamePathTest: TestDefinition = { @@ -18,7 +19,12 @@ export const simultaneousCreateDeleteSamePathTest: TestDefinition = { { type: "delete", client: 0, path: "A.md" }, { type: "sync", client: 0 }, - { type: "update", client: 1, path: "A.md", content: "modified by 1 while offline" }, + { + type: "update", + client: 1, + path: "A.md", + content: "modified by 1 while offline" + }, { type: "enable-sync", client: 1 }, { type: "sync", client: 1 }, @@ -26,14 +32,16 @@ export const simultaneousCreateDeleteSamePathTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => { - s.ifFileExists("A.md", (s) => - s.assertFileCount(1).assertContent("A.md", "modified by 1 while offline") + verify: (s: AssertableState): void => { + s.ifFileExists("A.md", (inner) => + inner + .assertFileCount(1) + .assertContent("A.md", "modified by 1 while offline") ); if (!s.files.has("A.md")) { s.assertFileCount(0); } - }, + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts index 174bcdc4..80478adc 100644 --- a/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/three-client-rename-create-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const threeClientRenameCreateDeleteTest: TestDefinition = { @@ -44,10 +45,11 @@ export const threeClientRenameCreateDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s - .assertFileNotExists("X.md") - .assertAnyFileContains("new from C"), + verify: (s: AssertableState): void => { + s.assertFileNotExists("X.md").assertAnyFileContains( + "new from C" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts index 43536bed..54c1beaf 100644 --- a/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts +++ b/frontend/deterministic-tests/src/tests/update-during-create-processing.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const updateDuringCreateProcessingTest: TestDefinition = { @@ -32,8 +33,12 @@ export const updateDuringCreateProcessingTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("file.md", "updated during create"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent( + "file.md", + "updated during create" + ); + } } ] }; diff --git a/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts b/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts index 09ec9427..70a2fc8c 100644 --- a/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts +++ b/frontend/deterministic-tests/src/tests/update-survives-remote-delete.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { @@ -14,7 +15,12 @@ export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { { type: "disable-sync", client: 1 }, { type: "delete", client: 0, path: "doc.md" }, - { type: "update", client: 1, path: "doc.md", content: "edited by client 1" }, + { + type: "update", + client: 1, + path: "doc.md", + content: "edited by client 1" + }, { type: "enable-sync", client: 0 }, { type: "enable-sync", client: 1 }, @@ -22,8 +28,9 @@ export const updateDoesNotSurvivesRemoteDeleteTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(0) - }, - ], + verify: (s: AssertableState): void => { + s.assertFileCount(0); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts index 202bd437..0212a19f 100644 --- a/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts +++ b/frontend/deterministic-tests/src/tests/watermark-advances-on-skip.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const watermarkAdvancesOnSkipTest: TestDefinition = { @@ -29,7 +30,9 @@ export const watermarkAdvancesOnSkipTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => s.assertFileCount(1).assertFileExists("doc.md"), - }, - ], + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertFileExists("doc.md"); + } + } + ] }; diff --git a/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts b/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts index 0f5ade3d..0ee606f0 100644 --- a/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts +++ b/frontend/deterministic-tests/src/tests/watermark-gap-remote-update-not-recorded.test.ts @@ -1,3 +1,4 @@ +import type { AssertableState } from "../utils/assertable-state"; import type { TestDefinition } from "../test-definition"; export const watermarkGapRemoteUpdateNotRecordedTest: TestDefinition = { @@ -20,8 +21,9 @@ export const watermarkGapRemoteUpdateNotRecordedTest: TestDefinition = { { type: "barrier" }, { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("doc.md", "update 2"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "update 2"); + } }, { type: "disable-sync", client: 1 }, @@ -31,8 +33,9 @@ export const watermarkGapRemoteUpdateNotRecordedTest: TestDefinition = { { type: "assert-consistent", - verify: (s) => - s.assertFileCount(1).assertContent("doc.md", "update 2"), + verify: (s: AssertableState): void => { + s.assertFileCount(1).assertContent("doc.md", "update 2"); + } } ] }; diff --git a/frontend/deterministic-tests/src/utils/assertable-state.ts b/frontend/deterministic-tests/src/utils/assertable-state.ts index 05414342..196333c0 100644 --- a/frontend/deterministic-tests/src/utils/assertable-state.ts +++ b/frontend/deterministic-tests/src/utils/assertable-state.ts @@ -1,15 +1,15 @@ import type { ClientState } from "../test-definition"; export class AssertableState { - readonly files: Map; - readonly clientFiles: Map[]; + public readonly files: Map; + public readonly clientFiles: Map[]; - constructor(state: ClientState) { + public constructor(state: ClientState) { this.files = state.files; this.clientFiles = state.clientFiles; } - assertFileCount(expected: number): this { + public assertFileCount(expected: number): this { if (this.files.size !== expected) { const keys = Array.from(this.files.keys()).join(", "); throw new Error( @@ -19,17 +19,15 @@ export class AssertableState { return this; } - assertFileExists(path: string): this { + public assertFileExists(path: string): this { if (!this.files.has(path)) { const keys = Array.from(this.files.keys()).join(", "); - throw new Error( - `Expected "${path}" to exist. Files: [${keys}]` - ); + throw new Error(`Expected "${path}" to exist. Files: [${keys}]`); } return this; } - assertFileNotExists(path: string): this { + public assertFileNotExists(path: string): this { if (this.files.has(path)) { const keys = Array.from(this.files.keys()).join(", "); throw new Error( @@ -39,7 +37,7 @@ export class AssertableState { return this; } - assertContent(path: string, expected: string): this { + public assertContent(path: string, expected: string): this { this.assertFileExists(path); const actual = this.files.get(path) ?? ""; if (actual !== expected) { @@ -50,7 +48,7 @@ export class AssertableState { return this; } - assertContains(path: string, ...substrings: string[]): this { + public assertContains(path: string, ...substrings: string[]): this { this.assertFileExists(path); const content = this.files.get(path) ?? ""; const missing = substrings.filter((s) => !content.includes(s)); @@ -62,7 +60,7 @@ export class AssertableState { return this; } - assertContainsAny(path: string, ...substrings: string[]): this { + public assertContainsAny(path: string, ...substrings: string[]): this { this.assertFileExists(path); const content = this.files.get(path) ?? ""; const found = substrings.some((s) => content.includes(s)); @@ -74,7 +72,7 @@ export class AssertableState { return this; } - assertAnyFileContains(...substrings: string[]): this { + public assertAnyFileContains(...substrings: string[]): this { const allContent = Array.from(this.files.values()).join("\n"); const missing = substrings.filter((s) => !allContent.includes(s)); if (missing.length > 0) { @@ -88,7 +86,7 @@ export class AssertableState { return this; } - assertSubstringCount( + public assertSubstringCount( path: string, substring: string, expected: number @@ -104,7 +102,7 @@ export class AssertableState { return this; } - assertContentInAtMostOneFile(substring: string): this { + public assertContentInAtMostOneFile(substring: string): this { const matches = Array.from(this.files.entries()).filter(([, content]) => content.includes(substring) ); @@ -119,14 +117,14 @@ export class AssertableState { return this; } - ifFileExists(path: string, fn: (state: this) => void): this { + public ifFileExists(path: string, fn: (state: this) => void): this { if (this.files.has(path)) { fn(this); } return this; } - getContent(path: string): string { + public getContent(path: string): string { return this.files.get(path) ?? ""; } } diff --git a/frontend/history-ui/src/lib/api.ts b/frontend/history-ui/src/lib/api.ts index a69a575e..7c365b57 100644 --- a/frontend/history-ui/src/lib/api.ts +++ b/frontend/history-ui/src/lib/api.ts @@ -28,9 +28,7 @@ async function fetchJsonWithToken( return response.json() as Promise; } -export async function listVaults( - token: string -): Promise { +export async function listVaults(token: string): Promise { return fetchJsonWithToken("/vaults", token); } @@ -44,10 +42,7 @@ export class ApiClient { return `/vaults/${encodeURIComponent(this.vaultId)}`; } - private async fetchJson( - path: string, - init?: RequestInit - ): Promise { + private async fetchJson(path: string, init?: RequestInit): Promise { return fetchJsonWithToken(path, this.token, init); } @@ -104,9 +99,7 @@ export class ApiClient { if (beforeUpdateId !== undefined) params.set("before_update_id", String(beforeUpdateId)); const qs = params.toString(); - return this.fetchJson( - `${this.baseUrl}/history${qs ? `?${qs}` : ""}` - ); + return this.fetchJson(`${this.baseUrl}/history${qs ? `?${qs}` : ""}`); } /** diff --git a/frontend/history-ui/src/lib/stores.svelte.ts b/frontend/history-ui/src/lib/stores.svelte.ts index fcba5340..458ec5e7 100644 --- a/frontend/history-ui/src/lib/stores.svelte.ts +++ b/frontend/history-ui/src/lib/stores.svelte.ts @@ -16,11 +16,7 @@ class AuthStore { isAuthenticated = $state(false); api = $state(null); - authenticate( - token: string, - userName: string, - vaults: VaultInfo[] - ) { + authenticate(token: string, userName: string, vaults: VaultInfo[]) { this.token = token; this.userName = userName; this.availableVaults = vaults; @@ -56,8 +52,7 @@ class AuthStore { tryRestore(): { token: string; vaultId?: string } | null { const token = sessionStorage.getItem("vaultlink_token"); if (!token) return null; - const vaultId = - sessionStorage.getItem("vaultlink_vault") ?? undefined; + const vaultId = sessionStorage.getItem("vaultlink_vault") ?? undefined; return { token, vaultId }; } } @@ -115,13 +110,8 @@ export function inferAction( ): ActionType { if (version.isDeleted) return "deleted"; if (!previousVersion) return "created"; - if ( - previousVersion.isDeleted && - !version.isDeleted - ) - return "restored"; - if (previousVersion.relativePath !== version.relativePath) - return "renamed"; + if (previousVersion.isDeleted && !version.isDeleted) return "restored"; + if (previousVersion.relativePath !== version.relativePath) return "renamed"; return "updated"; } @@ -150,8 +140,7 @@ export function enrichVersions( return { ...v, action, - previousPath: - action === "renamed" ? prev?.relativePath : undefined + previousPath: action === "renamed" ? prev?.relativePath : undefined }; }); } diff --git a/frontend/history-ui/src/lib/types/ClientCursors.ts b/frontend/history-ui/src/lib/types/ClientCursors.ts index bb629100..14298431 100644 --- a/frontend/history-ui/src/lib/types/ClientCursors.ts +++ b/frontend/history-ui/src/lib/types/ClientCursors.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DocumentWithCursors } from "./DocumentWithCursors"; -export type ClientCursors = { userName: string, deviceId: string, documentsWithCursors: Array, }; +export type ClientCursors = { + userName: string; + deviceId: string; + documentsWithCursors: Array; +}; diff --git a/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts b/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts index 29d3f55e..389d8e88 100644 --- a/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts +++ b/frontend/history-ui/src/lib/types/CreateDocumentVersion.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CreateDocumentVersion = { relative_path: string, last_seen_vault_update_id: number, content: Array, }; +export type CreateDocumentVersion = { + relative_path: string; + last_seen_vault_update_id: number; + content: Array; +}; diff --git a/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts b/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts index 60b48e5e..5846843e 100644 --- a/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts +++ b/frontend/history-ui/src/lib/types/CursorPositionFromClient.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DocumentWithCursors } from "./DocumentWithCursors"; -export type CursorPositionFromClient = { documentsWithCursors: Array, }; +export type CursorPositionFromClient = { + documentsWithCursors: Array; +}; diff --git a/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts b/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts index c8444892..3a72c706 100644 --- a/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts +++ b/frontend/history-ui/src/lib/types/CursorPositionFromServer.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ClientCursors } from "./ClientCursors"; -export type CursorPositionFromServer = { clients: Array, }; +export type CursorPositionFromServer = { clients: Array }; diff --git a/frontend/history-ui/src/lib/types/CursorSpan.ts b/frontend/history-ui/src/lib/types/CursorSpan.ts index d0bce6ea..916019ce 100644 --- a/frontend/history-ui/src/lib/types/CursorSpan.ts +++ b/frontend/history-ui/src/lib/types/CursorSpan.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CursorSpan = { start: number, end: number, }; +export type CursorSpan = { start: number; end: number }; diff --git a/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts b/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts index 51e0b37c..dd7eadda 100644 --- a/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts +++ b/frontend/history-ui/src/lib/types/DocumentUpdateResponse.ts @@ -5,4 +5,6 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a create/update document request. */ -export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentVersionWithoutContent | { "type": "MergingUpdate" } & DocumentVersion; +export type DocumentUpdateResponse = + | ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent) + | ({ type: "MergingUpdate" } & DocumentVersion); diff --git a/frontend/history-ui/src/lib/types/DocumentVersion.ts b/frontend/history-ui/src/lib/types/DocumentVersion.ts index 37bd32ca..50a6c591 100644 --- a/frontend/history-ui/src/lib/types/DocumentVersion.ts +++ b/frontend/history-ui/src/lib/types/DocumentVersion.ts @@ -1,3 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DocumentVersion = { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, contentBase64: string, isDeleted: boolean, userId: string, deviceId: string, }; +export type DocumentVersion = { + vaultUpdateId: number; + documentId: string; + relativePath: string; + updatedDate: string; + contentBase64: string; + isDeleted: boolean; + userId: string; + deviceId: string; +}; diff --git a/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts b/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts index 03be2f63..dad1f135 100644 --- a/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts +++ b/frontend/history-ui/src/lib/types/DocumentVersionWithoutContent.ts @@ -1,3 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DocumentVersionWithoutContent = { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, isDeleted: boolean, userId: string, deviceId: string, contentSize: number, }; +export type DocumentVersionWithoutContent = { + vaultUpdateId: number; + documentId: string; + relativePath: string; + updatedDate: string; + isDeleted: boolean; + userId: string; + deviceId: string; + contentSize: number; +}; diff --git a/frontend/history-ui/src/lib/types/DocumentWithCursors.ts b/frontend/history-ui/src/lib/types/DocumentWithCursors.ts index 3504ce33..ca6a2155 100644 --- a/frontend/history-ui/src/lib/types/DocumentWithCursors.ts +++ b/frontend/history-ui/src/lib/types/DocumentWithCursors.ts @@ -1,4 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CursorSpan } from "./CursorSpan"; -export type DocumentWithCursors = { vaultUpdateId: number | null, documentId: string, relativePath: string, cursors: Array, }; +export type DocumentWithCursors = { + vaultUpdateId: number | null; + documentId: string; + relativePath: string; + cursors: Array; +}; diff --git a/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts b/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts index ce572684..141c2565 100644 --- a/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts +++ b/frontend/history-ui/src/lib/types/FetchLatestDocumentsResponse.ts @@ -4,8 +4,10 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a fetch latest documents request. */ -export type FetchLatestDocumentsResponse = { latestDocuments: Array, -/** - * The update ID of the latest document in the response. - */ -lastUpdateId: bigint, }; +export type FetchLatestDocumentsResponse = { + latestDocuments: Array; + /** + * The update ID of the latest document in the response. + */ + lastUpdateId: bigint; +}; diff --git a/frontend/history-ui/src/lib/types/ListVaultsResponse.ts b/frontend/history-ui/src/lib/types/ListVaultsResponse.ts index 92b2b3e0..604ad958 100644 --- a/frontend/history-ui/src/lib/types/ListVaultsResponse.ts +++ b/frontend/history-ui/src/lib/types/ListVaultsResponse.ts @@ -4,4 +4,8 @@ import type { VaultInfo } from "./VaultInfo"; /** * Response to listing vaults accessible to the authenticated user. */ -export type ListVaultsResponse = { vaults: Array, hasMore: boolean, userName: string, }; +export type ListVaultsResponse = { + vaults: Array; + hasMore: boolean; + userName: string; +}; diff --git a/frontend/history-ui/src/lib/types/PingResponse.ts b/frontend/history-ui/src/lib/types/PingResponse.ts index c38845d2..7e5ac4f8 100644 --- a/frontend/history-ui/src/lib/types/PingResponse.ts +++ b/frontend/history-ui/src/lib/types/PingResponse.ts @@ -3,22 +3,23 @@ /** * Response to a ping request. */ -export type PingResponse = { -/** - * Semantic version of the server. - */ -serverVersion: string, -/** - * Whether the client is authenticated based on the sent Authorization - * header. - */ -isAuthenticated: boolean, -/** - * List of file extensions that are allowed to be merged. - */ -mergeableFileExtensions: Array, -/** - * API version ensuring backwards & forwards compatibility between the client - * and server. - */ -supportedApiVersion: number, }; +export type PingResponse = { + /** + * Semantic version of the server. + */ + serverVersion: string; + /** + * Whether the client is authenticated based on the sent Authorization + * header. + */ + isAuthenticated: boolean; + /** + * List of file extensions that are allowed to be merged. + */ + mergeableFileExtensions: Array; + /** + * API version ensuring backwards & forwards compatibility between the client + * and server. + */ + supportedApiVersion: number; +}; diff --git a/frontend/history-ui/src/lib/types/SerializedError.ts b/frontend/history-ui/src/lib/types/SerializedError.ts index 5e3fa9b9..354305f6 100644 --- a/frontend/history-ui/src/lib/types/SerializedError.ts +++ b/frontend/history-ui/src/lib/types/SerializedError.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SerializedError = { errorType: string, message: string, causes: Array, }; +export type SerializedError = { + errorType: string; + message: string; + causes: Array; +}; diff --git a/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts b/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts index 458fc2bb..ce0272e3 100644 --- a/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts +++ b/frontend/history-ui/src/lib/types/UpdateTextDocumentVersion.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UpdateTextDocumentVersion = { parentVersionId: number, relativePath: string, content: Array, }; +export type UpdateTextDocumentVersion = { + parentVersionId: number; + relativePath: string; + content: Array; +}; diff --git a/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts b/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts index ae91b480..e69366f0 100644 --- a/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts +++ b/frontend/history-ui/src/lib/types/VaultHistoryResponse.ts @@ -4,4 +4,7 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a vault history request (paginated). */ -export type VaultHistoryResponse = { versions: Array, hasMore: boolean, }; +export type VaultHistoryResponse = { + versions: Array; + hasMore: boolean; +}; diff --git a/frontend/history-ui/src/lib/types/VaultInfo.ts b/frontend/history-ui/src/lib/types/VaultInfo.ts index 32373346..3f630ae9 100644 --- a/frontend/history-ui/src/lib/types/VaultInfo.ts +++ b/frontend/history-ui/src/lib/types/VaultInfo.ts @@ -3,4 +3,8 @@ /** * Summary of a single vault returned by the list-vaults endpoint. */ -export type VaultInfo = { name: string, documentCount: number, createdAt: string | null, }; +export type VaultInfo = { + name: string; + documentCount: number; + createdAt: string | null; +}; diff --git a/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts b/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts index 5765a0d0..9608f3af 100644 --- a/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts +++ b/frontend/history-ui/src/lib/types/WebSocketClientMessage.ts @@ -2,4 +2,6 @@ import type { CursorPositionFromClient } from "./CursorPositionFromClient"; import type { WebSocketHandshake } from "./WebSocketHandshake"; -export type WebSocketClientMessage = { "type": "handshake" } & WebSocketHandshake | { "type": "cursorPositions" } & CursorPositionFromClient; +export type WebSocketClientMessage = + | ({ type: "handshake" } & WebSocketHandshake) + | ({ type: "cursorPositions" } & CursorPositionFromClient); diff --git a/frontend/history-ui/src/lib/types/WebSocketHandshake.ts b/frontend/history-ui/src/lib/types/WebSocketHandshake.ts index 85c2cf0d..8e51a121 100644 --- a/frontend/history-ui/src/lib/types/WebSocketHandshake.ts +++ b/frontend/history-ui/src/lib/types/WebSocketHandshake.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type WebSocketHandshake = { token: string, deviceId: string, lastSeenVaultUpdateId: number | null, }; +export type WebSocketHandshake = { + token: string; + deviceId: string; + lastSeenVaultUpdateId: number | null; +}; diff --git a/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts b/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts index 45e37358..fd250b7b 100644 --- a/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts +++ b/frontend/history-ui/src/lib/types/WebSocketServerMessage.ts @@ -2,4 +2,6 @@ import type { CursorPositionFromServer } from "./CursorPositionFromServer"; import type { WebSocketVaultUpdate } from "./WebSocketVaultUpdate"; -export type WebSocketServerMessage = { "type": "vaultUpdate" } & WebSocketVaultUpdate | { "type": "cursorPositions" } & CursorPositionFromServer; +export type WebSocketServerMessage = + | ({ type: "vaultUpdate" } & WebSocketVaultUpdate) + | ({ type: "cursorPositions" } & CursorPositionFromServer); diff --git a/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts b/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts index fc10827f..94d70c0a 100644 --- a/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts +++ b/frontend/history-ui/src/lib/types/WebSocketVaultUpdate.ts @@ -1,4 +1,4 @@ // 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"; -export type WebSocketVaultUpdate = { document: DocumentVersionWithoutContent, }; +export type WebSocketVaultUpdate = { document: DocumentVersionWithoutContent }; diff --git a/frontend/local-client-cli/src/args.ts b/frontend/local-client-cli/src/args.ts index 5124b72f..3652a4c7 100644 --- a/frontend/local-client-cli/src/args.ts +++ b/frontend/local-client-cli/src/args.ts @@ -37,22 +37,19 @@ export function parseArgs(argv: string[]): CliArgs { ).env("VAULTLINK_LOCAL_PATH") ) .addOption( - new Option( - "-r, --remote-uri ", - "Remote server URI" - ).env("VAULTLINK_REMOTE_URI") + new Option("-r, --remote-uri ", "Remote server URI").env( + "VAULTLINK_REMOTE_URI" + ) ) .addOption( - new Option( - "-t, --token ", - "Authentication token" - ).env("VAULTLINK_TOKEN") + new Option("-t, --token ", "Authentication token").env( + "VAULTLINK_TOKEN" + ) ) .addOption( - new Option( - "-v, --vault-name ", - "Vault name" - ).env("VAULTLINK_VAULT_NAME") + new Option("-v, --vault-name ", "Vault name").env( + "VAULTLINK_VAULT_NAME" + ) ) .addOption( new Option( @@ -147,10 +144,7 @@ Environment variables: const lineEndingsStr = (opts.lineEndings as string | undefined) ?? "auto"; /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */ - const requireOption = ( - value: T | undefined, - name: string - ): T => { + const requireOption = (value: T | undefined, name: string): T => { if (value === undefined) { const option = program.options.find( (o) => o.attributeName() === name @@ -173,9 +167,7 @@ Environment variables: // Validate remote URI protocol if ( - !VALID_PROTOCOLS.some((prefix) => - requiredRemoteUri.startsWith(prefix) - ) + !VALID_PROTOCOLS.some((prefix) => requiredRemoteUri.startsWith(prefix)) ) { throw new Error( `Invalid remote URI '${requiredRemoteUri}'. Must start with ${VALID_PROTOCOLS.join(", ")}` diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index 1a8b1e83..e06fda47 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -50,9 +50,7 @@ function createLogHandler(minLevel: LogLevel): (logLine: LogLine) => void { const HEALTH_CHECK_INTERVAL_MS = 30 * 1000; const PROGRESS_LOG_INTERVAL_MS = 2000; -function resolveLineEndings( - mode: "auto" | "lf" | "crlf" -): string { +function resolveLineEndings(mode: "auto" | "lf" | "crlf"): string { switch (mode) { case "lf": return "\n"; @@ -94,9 +92,7 @@ async function main(): Promise { logger.info(`Remote URI: ${args.remoteUri}`); logger.info(`Vault name: ${args.vaultName}`); if (args.lineEndings !== "auto") { - logger.info( - `Line endings: ${args.lineEndings.toUpperCase()}` - ); + logger.info(`Line endings: ${args.lineEndings.toUpperCase()}`); } } @@ -138,9 +134,7 @@ async function main(): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion database = JSON.parse(content) as Partial; } catch { - logger.warn( - `Cannot read data file at ${dataFile}` - ); + logger.warn(`Cannot read data file at ${dataFile}`); } return { @@ -225,9 +219,7 @@ async function main(): Promise { } isShuttingDown = true; - client.logger.info( - `${signal} received, shutting down gracefully` - ); + client.logger.info(`${signal} received, shutting down gracefully`); fileWatcher.stop(); await client.waitUntilFinished(); diff --git a/frontend/local-client-cli/src/file-watcher.ts b/frontend/local-client-cli/src/file-watcher.ts index 2e70df02..c273a412 100644 --- a/frontend/local-client-cli/src/file-watcher.ts +++ b/frontend/local-client-cli/src/file-watcher.ts @@ -28,8 +28,7 @@ export class FileWatcher { renameDetection: true, renameTimeout: 125, ignoreInitial: true, - ignore: (filePath: string): boolean => - this.shouldIgnore(filePath) + ignore: (filePath: string): boolean => this.shouldIgnore(filePath) }); this.watcher.on("add", (filePath: string) => { @@ -91,8 +90,4 @@ export class FileWatcher { private toRelativePath(absolutePath: string): RelativePath { return toUnixPath(path.relative(this.basePath, absolutePath)); } - - private formatError(err: unknown): string { - return err instanceof Error ? err.message : String(err); - } } diff --git a/frontend/local-client-cli/src/node-filesystem.ts b/frontend/local-client-cli/src/node-filesystem.ts index f84cbdb8..7b736c22 100644 --- a/frontend/local-client-cli/src/node-filesystem.ts +++ b/frontend/local-client-cli/src/node-filesystem.ts @@ -15,18 +15,12 @@ export class NodeFileSystemOperations implements FileSystemOperations { directory: RelativePath | undefined ): Promise { const files: RelativePath[] = []; - await this.walkDirectory( - directory ?? "", - files - ); + await this.walkDirectory(directory ?? "", files); return files; } public async read(relativePath: RelativePath): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { return await fs.readFile(fullPath); } catch (error) { @@ -40,10 +34,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { relativePath: RelativePath, content: Uint8Array ): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); const dir = path.dirname(fullPath); try { @@ -60,10 +51,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { relativePath: RelativePath, updater: (current: TextWithCursors) => TextWithCursors ): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { const currentContent = await fs.readFile(fullPath, "utf-8"); @@ -78,10 +66,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { } public async getFileSize(relativePath: RelativePath): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { const stats = await fs.stat(fullPath); return stats.size; @@ -93,10 +78,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { } public async exists(relativePath: RelativePath): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { await fs.access(fullPath); return true; @@ -106,10 +88,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { } public async createDirectory(relativePath: RelativePath): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { await fs.mkdir(fullPath, { recursive: false }); } catch (error) { @@ -120,10 +99,7 @@ export class NodeFileSystemOperations implements FileSystemOperations { } public async delete(relativePath: RelativePath): Promise { - const fullPath = path.join( - this.basePath, - relativePath - ); + const fullPath = path.join(this.basePath, relativePath); try { await fs.unlink(fullPath); } catch (error) { @@ -191,5 +167,4 @@ export class NodeFileSystemOperations implements FileSystemOperations { } } } - } diff --git a/frontend/local-client-cli/src/path-utils.ts b/frontend/local-client-cli/src/path-utils.ts index a781b746..dd89fa67 100644 --- a/frontend/local-client-cli/src/path-utils.ts +++ b/frontend/local-client-cli/src/path-utils.ts @@ -8,10 +8,7 @@ export function toUnixPath(nativePath: string): string { // Match a file path against a glob pattern // Extends path.matchesGlob so that "dir/**" also matches the directory itself export function matchesGlob(filePath: string, pattern: string): boolean { - if ( - pattern.endsWith("/**") && - filePath === pattern.slice(0, -3) - ) { + if (pattern.endsWith("/**") && filePath === pattern.slice(0, -3)) { return true; } return path.matchesGlob(filePath, pattern); diff --git a/frontend/obsidian-plugin/src/vault-link-plugin.ts b/frontend/obsidian-plugin/src/vault-link-plugin.ts index 0291e646..e222796b 100644 --- a/frontend/obsidian-plugin/src/vault-link-plugin.ts +++ b/frontend/obsidian-plugin/src/vault-link-plugin.ts @@ -231,9 +231,9 @@ export default class VaultLinkPlugin extends Plugin { } } ), - this.app.vault.on("create", async (file: TAbstractFile) => { + this.app.vault.on("create", (file: TAbstractFile) => { if (file instanceof TFile) { - await client.syncLocallyCreatedFile(file.path); + client.syncLocallyCreatedFile(file.path); } }), this.app.vault.on("modify", async (file: TAbstractFile) => { @@ -241,14 +241,14 @@ export default class VaultLinkPlugin extends Plugin { await this.rateLimitedUpdate(file.path, client); } }), - this.app.vault.on("delete", async (file: TAbstractFile) => { - await client.syncLocallyDeletedFile(file.path); + this.app.vault.on("delete", (file: TAbstractFile) => { + client.syncLocallyDeletedFile(file.path); }), this.app.vault.on( "rename", - async (file: TAbstractFile, oldPath: string) => { + (file: TAbstractFile, oldPath: string) => { if (file instanceof TFile) { - await client.syncLocallyUpdatedFile({ + client.syncLocallyUpdatedFile({ oldPath, relativePath: file.path }); @@ -267,13 +267,11 @@ export default class VaultLinkPlugin extends Plugin { if (!this.rateLimitedUpdatesPerFile.has(path)) { this.rateLimitedUpdatesPerFile.set( path, - rateLimit( - async () => - { client.syncLocallyUpdatedFile({ - relativePath: path - }); }, - MIN_WAIT_BETWEEN_UPDATES_IN_MS - ) + rateLimit(async () => { + client.syncLocallyUpdatedFile({ + relativePath: path + }); + }, MIN_WAIT_BETWEEN_UPDATES_IN_MS) ); } await this.rateLimitedUpdatesPerFile.get(path)?.(); diff --git a/frontend/sync-client/src/file-operations/file-operations.test.ts b/frontend/sync-client/src/file-operations/file-operations.test.ts index 5d1129db..12e3777d 100644 --- a/frontend/sync-client/src/file-operations/file-operations.test.ts +++ b/frontend/sync-client/src/file-operations/file-operations.test.ts @@ -8,6 +8,7 @@ import type { FileSystemOperations } from "./filesystem-operations"; import type { TextWithCursors } from "reconcile-text"; import type { ServerConfig, ServerConfigData } from "../services/server-config"; import { CONFLICT_PATH_REGEX } from "../sync-operations/conflict-path"; +import { removeFromArray } from "../utils/remove-from-array"; class MockServerConfig implements Pick { public async getConfig(): Promise { @@ -81,9 +82,7 @@ function singleConflictPath( expectedNonConflictNames: string[] ): string { const expected = new Set(expectedNonConflictNames); - const conflicts = Array.from(names).filter( - (name) => !expected.has(name) - ); + const conflicts = Array.from(names).filter((name) => !expected.has(name)); assert.equal( conflicts.length, 1, @@ -139,7 +138,11 @@ describe("File operations", () => { it("move with EXISTING displaces the target to a conflict path", async () => { const { fs, ops } = makeOps(); - await ops.create("source.md", new Uint8Array(), MoveOnConflict.EXISTING); + await ops.create( + "source.md", + new Uint8Array(), + MoveOnConflict.EXISTING + ); await ops.create("dest.md", new Uint8Array(), MoveOnConflict.EXISTING); await ops.move("source.md", "dest.md", MoveOnConflict.EXISTING); @@ -156,7 +159,11 @@ describe("File operations", () => { it("move with NEW redirects the moved file to a conflict path", async () => { const { fs, ops } = makeOps(); - await ops.create("source.md", new Uint8Array(), MoveOnConflict.EXISTING); + await ops.create( + "source.md", + new Uint8Array(), + MoveOnConflict.EXISTING + ); await ops.create("dest.md", new Uint8Array(), MoveOnConflict.EXISTING); await ops.move("source.md", "dest.md", MoveOnConflict.NEW); @@ -190,7 +197,11 @@ describe("File operations", () => { it("handles dotfiles without mangling the extension", async () => { const { fs, ops } = makeOps(); - await ops.create(".gitignore", new Uint8Array(), MoveOnConflict.EXISTING); + await ops.create( + ".gitignore", + new Uint8Array(), + MoveOnConflict.EXISTING + ); await ops.create("temp", new Uint8Array(), MoveOnConflict.EXISTING); await ops.move("temp", ".gitignore", MoveOnConflict.EXISTING); @@ -200,7 +211,11 @@ describe("File operations", () => { `conflict should preserve the dotfile name verbatim, got ${conflict}` ); - await ops.create(".config.json", new Uint8Array(), MoveOnConflict.EXISTING); + await ops.create( + ".config.json", + new Uint8Array(), + MoveOnConflict.EXISTING + ); await ops.create("temp2", new Uint8Array(), MoveOnConflict.EXISTING); await ops.move("temp2", ".config.json", MoveOnConflict.EXISTING); @@ -221,7 +236,8 @@ describe("File operations", () => { await ops.create("x", new Uint8Array(), MoveOnConflict.EXISTING); await ops.create("x", new Uint8Array(), MoveOnConflict.EXISTING); - const conflicts = Array.from(fs.names).filter((n) => n !== "x"); + const conflicts = Array.from(fs.names); + removeFromArray(conflicts, "x"); assert.equal(conflicts.length, 2); assert.ok(conflicts.every((c) => CONFLICT_PATH_REGEX.test(c))); assert.notEqual( diff --git a/frontend/sync-client/src/file-operations/file-operations.ts b/frontend/sync-client/src/file-operations/file-operations.ts index 5384768d..29e9f0b6 100644 --- a/frontend/sync-client/src/file-operations/file-operations.ts +++ b/frontend/sync-client/src/file-operations/file-operations.ts @@ -1,7 +1,6 @@ import type { Logger } from "../tracing/logger"; import type { FileSystemOperations } from "./filesystem-operations"; import type { RelativePath } from "../sync-operations/types"; -import type { SyncEventQueue } from "../sync-operations/sync-event-queue"; import { SafeFileSystemOperations } from "./safe-filesystem-operations"; import type { TextWithCursors } from "reconcile-text"; import { reconcile } from "reconcile-text"; @@ -10,10 +9,9 @@ import { isBinary } from "../utils/is-binary"; import { buildConflictFileName } from "../sync-operations/conflict-path"; import type { ServerConfig } from "../services/server-config"; - export enum MoveOnConflict { EXISTING = "EXISTING", - NEW = "NEW", + NEW = "NEW" } export class FileOperations { @@ -40,6 +38,17 @@ export class FileOperations { return [pathParts.join("/"), fileName]; } + /** + * Build a local-only conflict path for a file the client has to set aside. + * Format: `/conflict--` — UUID makes collisions + * statistically impossible, so no disk probe / lock dance is needed. + */ + private static buildConflictPath(path: RelativePath): RelativePath { + const [directory, fileName] = FileOperations.getParentDirAndFile(path); + const conflictName = buildConflictFileName(fileName); + return directory ? `${directory}/${conflictName}` : conflictName; + } + public async listFilesRecursively( root: RelativePath | undefined = undefined ): Promise { @@ -55,7 +64,7 @@ export class FileOperations { * * If a file with the same name already exists, it is moved before creating the new one. * Parent directories are created if necessary. - * + * * Returns the actual path the file was created at. */ public async create( @@ -68,30 +77,6 @@ export class FileOperations { return actualPath; } - private async ensureClearPath( - path: RelativePath, - moveOnConflict: MoveOnConflict - ): Promise { - if (await this.fs.exists(path)) { - const conflictPath = FileOperations.buildConflictPath(path); - - if (moveOnConflict === MoveOnConflict.NEW) { - return conflictPath; - } - - this.logger.debug( - `Displacing existing file at ${path} to '${conflictPath}' to make room` - ); - - await this.fs.rename(path, conflictPath); - return path; - } - - this.logger.debug(`No existing file at ${path}, creating parent directories if needed`); - await this.createParentDirectories(path); - return path; - } - /** * Update the file at the given path. * @@ -129,8 +114,8 @@ export class FileOperations { return; } - let expectedText: string; - let newText: string; + let expectedText = ""; + let newText = ""; try { expectedText = new TextDecoder("utf-8", { fatal: true }).decode( expectedContent @@ -206,6 +191,31 @@ export class FileOperations { return actualPath; } + private async ensureClearPath( + path: RelativePath, + moveOnConflict: MoveOnConflict + ): Promise { + if (await this.fs.exists(path)) { + const conflictPath = FileOperations.buildConflictPath(path); + + if (moveOnConflict === MoveOnConflict.NEW) { + return conflictPath; + } + + this.logger.debug( + `Displacing existing file at ${path} to '${conflictPath}' to make room` + ); + + await this.fs.rename(path, conflictPath); + return path; + } + + this.logger.debug( + `No existing file at ${path}, creating parent directories if needed` + ); + await this.createParentDirectories(path); + return path; + } private async deletingEmptyParentDirectoriesOfDeletedFile( path: RelativePath @@ -265,16 +275,4 @@ export class FileOperations { } } } - - /** - * Build a local-only conflict path for a file the client has to set aside. - * Format: `/conflict--` — UUID makes collisions - * statistically impossible, so no disk probe / lock dance is needed. - */ - private static buildConflictPath(path: RelativePath): RelativePath { - const [directory, fileName] = - FileOperations.getParentDirAndFile(path); - const conflictName = buildConflictFileName(fileName); - return directory ? `${directory}/${conflictName}` : conflictName; - } } diff --git a/frontend/sync-client/src/index.ts b/frontend/sync-client/src/index.ts index da69f446..c79ace63 100644 --- a/frontend/sync-client/src/index.ts +++ b/frontend/sync-client/src/index.ts @@ -22,7 +22,11 @@ export { export { Logger, LogLevel, LogLine } from "./tracing/logger"; export { type SyncSettings, DEFAULT_SETTINGS } from "./persistence/settings"; export { rateLimit } from "./utils/rate-limit"; -export type { RelativePath, StoredSyncState as StoredDatabase, DocumentRecord } from "./sync-operations/types"; +export type { + RelativePath, + StoredSyncState as StoredDatabase, + DocumentRecord +} from "./sync-operations/types"; export type { FileSystemOperations } from "./file-operations/filesystem-operations"; export type { PersistenceProvider } from "./persistence/persistence"; export type { CursorSpan } from "./services/types/CursorSpan"; diff --git a/frontend/sync-client/src/services/fetch-controller.ts b/frontend/sync-client/src/services/fetch-controller.ts index cf857dcd..f5bb8664 100644 --- a/frontend/sync-client/src/services/fetch-controller.ts +++ b/frontend/sync-client/src/services/fetch-controller.ts @@ -19,7 +19,11 @@ export class FetchController { private _canFetch: boolean, private readonly logger: Logger ) { - ({ promise: this.until, resolve: this.resolveUntil, reject: this.rejectUntil } = Promise.withResolvers()); + ({ + promise: this.until, + resolve: this.resolveUntil, + reject: this.rejectUntil + } = Promise.withResolvers()); } /** @@ -40,7 +44,11 @@ export class FetchController { if (!this.isResetting) { const previousResolve = this.resolveUntil; - ({ promise: this.until, resolve: this.resolveUntil, reject: this.rejectUntil } = Promise.withResolvers()); + ({ + promise: this.until, + resolve: this.resolveUntil, + reject: this.rejectUntil + } = Promise.withResolvers()); previousResolve(FetchController.UNTIL_RESOLUTION); } } @@ -78,7 +86,11 @@ export class FetchController { } this.isResetting = false; - ({ promise: this.until, resolve: this.resolveUntil, reject: this.rejectUntil } = Promise.withResolvers()); + ({ + promise: this.until, + resolve: this.resolveUntil, + reject: this.rejectUntil + } = Promise.withResolvers()); } /** diff --git a/frontend/sync-client/src/services/sync-service.ts b/frontend/sync-client/src/services/sync-service.ts index 873783c3..65726d73 100644 --- a/frontend/sync-client/src/services/sync-service.ts +++ b/frontend/sync-client/src/services/sync-service.ts @@ -66,6 +66,42 @@ export class SyncService { return result; } + private static async throwIfNotOk( + response: Response, + operation: string + ): Promise { + if (response.ok) return; + const message = `Failed to ${operation}: ${await SyncService.errorFromResponse(response)}`; + // 429 is the only 4xx the server uses for *transient* contention + // (`WriteBusyError` → HTTP 429). Every other 4xx means the request + // is permanently rejected and shouldn't be retried. + if (response.status === 429) { + throw new Error(message); + } + if (response.status >= 400 && response.status < 500) { + throw new HttpClientError(response.status, message); + } + throw new Error(message); + } + + /** + * Signal that the service is shutting down so any in-flight + * `retryForever` exits at its next iteration instead of looping + * indefinitely after the rest of the client has stopped. Idempotent. + */ + public stop(): void { + this.isStopped = true; + } + + /** + * Re-enable the service after a `stop()`. Used when the client pauses + * and resumes syncing within the same lifecycle (e.g. user toggles + * sync off and on). + */ + public resume(): void { + this.isStopped = false; + } + public async create({ relativePath, lastSeenVaultUpdateId, @@ -146,8 +182,7 @@ export class SyncService { (await response.json()) as DocumentUpdateResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion this.logger.debug( - `Updated document ${JSON.stringify(result)} with id ${ - result.documentId + `Updated document ${JSON.stringify(result)} with id ${result.documentId }}` ); @@ -193,8 +228,7 @@ export class SyncService { (await response.json()) as DocumentUpdateResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion this.logger.debug( - `Updated document ${JSON.stringify(result)} with id ${ - result.documentId + `Updated document ${JSON.stringify(result)} with id ${result.documentId }}` ); @@ -284,7 +318,10 @@ export class SyncService { } ); - await SyncService.throwIfNotOk(response, "get document version content"); + await SyncService.throwIfNotOk( + response, + "get document version content" + ); const result = await response.bytes(); this.logger.debug( @@ -300,7 +337,7 @@ export class SyncService { return this.retryForever(async () => { this.logger.debug( "Getting all documents" + - (since != null ? ` since ${since}` : "") + (since != null ? ` since ${since}` : "") ); const url = new URL(this.getUrl("/documents")); @@ -369,30 +406,10 @@ export class SyncService { return headers; } - /** - * Signal that the service is shutting down so any in-flight - * `retryForever` exits at its next iteration instead of looping - * indefinitely after the rest of the client has stopped. Idempotent. - */ - public stop(): void { - this.isStopped = true; - } - - /** - * Re-enable the service after a `stop()`. Used when the client pauses - * and resumes syncing within the same lifecycle (e.g. user toggles - * sync off and on). - */ - public resume(): void { - this.isStopped = false; - } - private async retryForever(fn: () => Promise): Promise { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { - if (this.isStopped) { - throw new SyncResetError(); - } + this.throwIfStopped(); try { return await fn(); } catch (e) { @@ -402,9 +419,7 @@ export class SyncService { ) { throw e; } - if (this.isStopped) { - throw new SyncResetError(); - } + this.throwIfStopped(); const retryInterval = this.settings.getSettings().networkRetryIntervalMs; @@ -416,21 +431,9 @@ export class SyncService { } } - private static async throwIfNotOk( - response: Response, - operation: string - ): Promise { - if (response.ok) return; - const message = `Failed to ${operation}: ${await SyncService.errorFromResponse(response)}`; - // 429 is the only 4xx the server uses for *transient* contention - // (`WriteBusyError` → HTTP 429). Every other 4xx means the request - // is permanently rejected and shouldn't be retried. - if (response.status === 429) { - throw new Error(message); + private throwIfStopped(): void { + if (this.isStopped) { + throw new SyncResetError(); } - if (response.status >= 400 && response.status < 500) { - throw new HttpClientError(response.status, message); - } - throw new Error(message); } } diff --git a/frontend/sync-client/src/services/types/ClientCursors.ts b/frontend/sync-client/src/services/types/ClientCursors.ts index 5b1ec040..e8c9b93d 100644 --- a/frontend/sync-client/src/services/types/ClientCursors.ts +++ b/frontend/sync-client/src/services/types/ClientCursors.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DocumentWithCursors } from "./DocumentWithCursors"; -export interface ClientCursors { userName: string, deviceId: string, documentsWithCursors: DocumentWithCursors[], } +export interface ClientCursors { + userName: string; + deviceId: string; + documentsWithCursors: DocumentWithCursors[]; +} diff --git a/frontend/sync-client/src/services/types/CreateDocumentVersion.ts b/frontend/sync-client/src/services/types/CreateDocumentVersion.ts index 4d1b324e..2d83cd99 100644 --- a/frontend/sync-client/src/services/types/CreateDocumentVersion.ts +++ b/frontend/sync-client/src/services/types/CreateDocumentVersion.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface CreateDocumentVersion { relative_path: string, last_seen_vault_update_id: number, content: number[], } +export interface CreateDocumentVersion { + relative_path: string; + last_seen_vault_update_id: number; + content: number[]; +} diff --git a/frontend/sync-client/src/services/types/CursorPositionFromClient.ts b/frontend/sync-client/src/services/types/CursorPositionFromClient.ts index 78823b5d..ee937f4e 100644 --- a/frontend/sync-client/src/services/types/CursorPositionFromClient.ts +++ b/frontend/sync-client/src/services/types/CursorPositionFromClient.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DocumentWithCursors } from "./DocumentWithCursors"; -export interface CursorPositionFromClient { documentsWithCursors: DocumentWithCursors[], } +export interface CursorPositionFromClient { + documentsWithCursors: DocumentWithCursors[]; +} diff --git a/frontend/sync-client/src/services/types/CursorPositionFromServer.ts b/frontend/sync-client/src/services/types/CursorPositionFromServer.ts index ed6ac7b2..52a24f27 100644 --- a/frontend/sync-client/src/services/types/CursorPositionFromServer.ts +++ b/frontend/sync-client/src/services/types/CursorPositionFromServer.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ClientCursors } from "./ClientCursors"; -export interface CursorPositionFromServer { clients: ClientCursors[], } +export interface CursorPositionFromServer { + clients: ClientCursors[]; +} diff --git a/frontend/sync-client/src/services/types/CursorSpan.ts b/frontend/sync-client/src/services/types/CursorSpan.ts index 7424067c..2cc2b7fc 100644 --- a/frontend/sync-client/src/services/types/CursorSpan.ts +++ b/frontend/sync-client/src/services/types/CursorSpan.ts @@ -1,3 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface CursorSpan { start: number, end: number, } +export interface CursorSpan { + start: number; + end: number; +} diff --git a/frontend/sync-client/src/services/types/DocumentUpdateResponse.ts b/frontend/sync-client/src/services/types/DocumentUpdateResponse.ts index 51e0b37c..dd7eadda 100644 --- a/frontend/sync-client/src/services/types/DocumentUpdateResponse.ts +++ b/frontend/sync-client/src/services/types/DocumentUpdateResponse.ts @@ -5,4 +5,6 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a create/update document request. */ -export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentVersionWithoutContent | { "type": "MergingUpdate" } & DocumentVersion; +export type DocumentUpdateResponse = + | ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent) + | ({ type: "MergingUpdate" } & DocumentVersion); diff --git a/frontend/sync-client/src/services/types/DocumentVersion.ts b/frontend/sync-client/src/services/types/DocumentVersion.ts index 3d50ae65..3b9aa37b 100644 --- a/frontend/sync-client/src/services/types/DocumentVersion.ts +++ b/frontend/sync-client/src/services/types/DocumentVersion.ts @@ -1,3 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface DocumentVersion { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, contentBase64: string, isDeleted: boolean, userId: string, deviceId: string, } +export interface DocumentVersion { + vaultUpdateId: number; + documentId: string; + relativePath: string; + updatedDate: string; + contentBase64: string; + isDeleted: boolean; + userId: string; + deviceId: string; +} diff --git a/frontend/sync-client/src/services/types/DocumentVersionWithoutContent.ts b/frontend/sync-client/src/services/types/DocumentVersionWithoutContent.ts index af064db8..4b24e7c5 100644 --- a/frontend/sync-client/src/services/types/DocumentVersionWithoutContent.ts +++ b/frontend/sync-client/src/services/types/DocumentVersionWithoutContent.ts @@ -1,3 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface DocumentVersionWithoutContent { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, isDeleted: boolean, userId: string, deviceId: string, contentSize: number, } +export interface DocumentVersionWithoutContent { + vaultUpdateId: number; + documentId: string; + relativePath: string; + updatedDate: string; + isDeleted: boolean; + userId: string; + deviceId: string; + contentSize: number; +} diff --git a/frontend/sync-client/src/services/types/DocumentWithCursors.ts b/frontend/sync-client/src/services/types/DocumentWithCursors.ts index d29b3f79..8ed59067 100644 --- a/frontend/sync-client/src/services/types/DocumentWithCursors.ts +++ b/frontend/sync-client/src/services/types/DocumentWithCursors.ts @@ -1,4 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CursorSpan } from "./CursorSpan"; -export interface DocumentWithCursors { vaultUpdateId: number | null, documentId: string, relativePath: string, cursors: CursorSpan[], } +export interface DocumentWithCursors { + vaultUpdateId: number | null; + documentId: string; + relativePath: string; + cursors: CursorSpan[]; +} diff --git a/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts b/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts index 3be625bd..315d701a 100644 --- a/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts +++ b/frontend/sync-client/src/services/types/FetchLatestDocumentsResponse.ts @@ -4,8 +4,10 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a fetch latest documents request. */ -export interface FetchLatestDocumentsResponse { latestDocuments: DocumentVersionWithoutContent[], -/** - * The update ID of the latest document in the response. - */ -lastUpdateId: bigint, } +export interface FetchLatestDocumentsResponse { + latestDocuments: DocumentVersionWithoutContent[]; + /** + * The update ID of the latest document in the response. + */ + lastUpdateId: bigint; +} diff --git a/frontend/sync-client/src/services/types/ListVaultsResponse.ts b/frontend/sync-client/src/services/types/ListVaultsResponse.ts index 85928d89..babad2d5 100644 --- a/frontend/sync-client/src/services/types/ListVaultsResponse.ts +++ b/frontend/sync-client/src/services/types/ListVaultsResponse.ts @@ -4,4 +4,8 @@ import type { VaultInfo } from "./VaultInfo"; /** * Response to listing vaults accessible to the authenticated user. */ -export interface ListVaultsResponse { vaults: VaultInfo[], hasMore: boolean, userName: string, } +export interface ListVaultsResponse { + vaults: VaultInfo[]; + hasMore: boolean; + userName: string; +} diff --git a/frontend/sync-client/src/services/types/PingResponse.ts b/frontend/sync-client/src/services/types/PingResponse.ts index ba8ceb48..f96520e9 100644 --- a/frontend/sync-client/src/services/types/PingResponse.ts +++ b/frontend/sync-client/src/services/types/PingResponse.ts @@ -3,22 +3,23 @@ /** * Response to a ping request. */ -export interface PingResponse { -/** - * Semantic version of the server. - */ -serverVersion: string, -/** - * Whether the client is authenticated based on the sent Authorization - * header. - */ -isAuthenticated: boolean, -/** - * List of file extensions that are allowed to be merged. - */ -mergeableFileExtensions: string[], -/** - * API version ensuring backwards & forwards compatibility between the client - * and server. - */ -supportedApiVersion: number, } +export interface PingResponse { + /** + * Semantic version of the server. + */ + serverVersion: string; + /** + * Whether the client is authenticated based on the sent Authorization + * header. + */ + isAuthenticated: boolean; + /** + * List of file extensions that are allowed to be merged. + */ + mergeableFileExtensions: string[]; + /** + * API version ensuring backwards & forwards compatibility between the client + * and server. + */ + supportedApiVersion: number; +} diff --git a/frontend/sync-client/src/services/types/SerializedError.ts b/frontend/sync-client/src/services/types/SerializedError.ts index 4389289e..ec1c4503 100644 --- a/frontend/sync-client/src/services/types/SerializedError.ts +++ b/frontend/sync-client/src/services/types/SerializedError.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface SerializedError { errorType: string, message: string, causes: string[], } +export interface SerializedError { + errorType: string; + message: string; + causes: string[]; +} diff --git a/frontend/sync-client/src/services/types/UpdateTextDocumentVersion.ts b/frontend/sync-client/src/services/types/UpdateTextDocumentVersion.ts index aeb69f5a..46f36bd0 100644 --- a/frontend/sync-client/src/services/types/UpdateTextDocumentVersion.ts +++ b/frontend/sync-client/src/services/types/UpdateTextDocumentVersion.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface UpdateTextDocumentVersion { parentVersionId: number, relativePath: string, content: (number | string)[], } +export interface UpdateTextDocumentVersion { + parentVersionId: number; + relativePath: string; + content: (number | string)[]; +} diff --git a/frontend/sync-client/src/services/types/VaultHistoryResponse.ts b/frontend/sync-client/src/services/types/VaultHistoryResponse.ts index 93d6ec6c..35531010 100644 --- a/frontend/sync-client/src/services/types/VaultHistoryResponse.ts +++ b/frontend/sync-client/src/services/types/VaultHistoryResponse.ts @@ -4,4 +4,7 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont /** * Response to a vault history request (paginated). */ -export interface VaultHistoryResponse { versions: DocumentVersionWithoutContent[], hasMore: boolean, } +export interface VaultHistoryResponse { + versions: DocumentVersionWithoutContent[]; + hasMore: boolean; +} diff --git a/frontend/sync-client/src/services/types/VaultInfo.ts b/frontend/sync-client/src/services/types/VaultInfo.ts index 921645f3..20d6811c 100644 --- a/frontend/sync-client/src/services/types/VaultInfo.ts +++ b/frontend/sync-client/src/services/types/VaultInfo.ts @@ -3,4 +3,8 @@ /** * Summary of a single vault returned by the list-vaults endpoint. */ -export interface VaultInfo { name: string, documentCount: number, createdAt: string | null, } +export interface VaultInfo { + name: string; + documentCount: number; + createdAt: string | null; +} diff --git a/frontend/sync-client/src/services/types/WebSocketClientMessage.ts b/frontend/sync-client/src/services/types/WebSocketClientMessage.ts index 5765a0d0..9608f3af 100644 --- a/frontend/sync-client/src/services/types/WebSocketClientMessage.ts +++ b/frontend/sync-client/src/services/types/WebSocketClientMessage.ts @@ -2,4 +2,6 @@ import type { CursorPositionFromClient } from "./CursorPositionFromClient"; import type { WebSocketHandshake } from "./WebSocketHandshake"; -export type WebSocketClientMessage = { "type": "handshake" } & WebSocketHandshake | { "type": "cursorPositions" } & CursorPositionFromClient; +export type WebSocketClientMessage = + | ({ type: "handshake" } & WebSocketHandshake) + | ({ type: "cursorPositions" } & CursorPositionFromClient); diff --git a/frontend/sync-client/src/services/types/WebSocketHandshake.ts b/frontend/sync-client/src/services/types/WebSocketHandshake.ts index d25651f9..a2910f49 100644 --- a/frontend/sync-client/src/services/types/WebSocketHandshake.ts +++ b/frontend/sync-client/src/services/types/WebSocketHandshake.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface WebSocketHandshake { token: string, deviceId: string, lastSeenVaultUpdateId: number | null, } +export interface WebSocketHandshake { + token: string; + deviceId: string; + lastSeenVaultUpdateId: number | null; +} diff --git a/frontend/sync-client/src/services/types/WebSocketServerMessage.ts b/frontend/sync-client/src/services/types/WebSocketServerMessage.ts index 45e37358..fd250b7b 100644 --- a/frontend/sync-client/src/services/types/WebSocketServerMessage.ts +++ b/frontend/sync-client/src/services/types/WebSocketServerMessage.ts @@ -2,4 +2,6 @@ import type { CursorPositionFromServer } from "./CursorPositionFromServer"; import type { WebSocketVaultUpdate } from "./WebSocketVaultUpdate"; -export type WebSocketServerMessage = { "type": "vaultUpdate" } & WebSocketVaultUpdate | { "type": "cursorPositions" } & CursorPositionFromServer; +export type WebSocketServerMessage = + | ({ type: "vaultUpdate" } & WebSocketVaultUpdate) + | ({ type: "cursorPositions" } & CursorPositionFromServer); diff --git a/frontend/sync-client/src/services/types/WebSocketVaultUpdate.ts b/frontend/sync-client/src/services/types/WebSocketVaultUpdate.ts index 5e7df8a5..b4a942c8 100644 --- a/frontend/sync-client/src/services/types/WebSocketVaultUpdate.ts +++ b/frontend/sync-client/src/services/types/WebSocketVaultUpdate.ts @@ -1,4 +1,6 @@ // 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"; -export interface WebSocketVaultUpdate { document: DocumentVersionWithoutContent, } +export interface WebSocketVaultUpdate { + document: DocumentVersionWithoutContent; +} diff --git a/frontend/sync-client/src/services/websocket-manager.ts b/frontend/sync-client/src/services/websocket-manager.ts index 970defb3..5279d0e6 100644 --- a/frontend/sync-client/src/services/websocket-manager.ts +++ b/frontend/sync-client/src/services/websocket-manager.ts @@ -58,8 +58,10 @@ export class WebSocketManager { } public async stop(): Promise { - const { promise, resolve } = Promise.withResolvers(); - this.resolveDisconnectingPromise = resolve; + const { promise, resolve } = Promise.withResolvers(); + this.resolveDisconnectingPromise = (): void => { + resolve(undefined); + }; this.isStopped = true; diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index 902c7b26..9c919354 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -46,7 +46,6 @@ export class SyncClient { private readonly cursorTracker: CursorTracker, private readonly fileChangeNotifier: FileChangeNotifier, private readonly contentCache: FixedSizeDocumentCache, - private readonly fileOperations: FileOperations, private readonly serverConfig: ServerConfig, private readonly syncService: SyncService, private readonly persistence: PersistenceProvider< @@ -100,6 +99,13 @@ export class SyncClient { return this.cursorTracker.onRemoteCursorsUpdated; } + public get hasPendingWork(): boolean { + return ( + this.syncEventQueue.pendingUpdateCount > 0 || + this.webSocketManager.hasOutstandingWork + ); + } + public static async create({ fs, persistence, @@ -219,7 +225,6 @@ export class SyncClient { cursorTracker, fileChangeNotifier, contentCache, - fileOperations, serverConfig, syncService, persistence @@ -323,7 +328,7 @@ export class SyncClient { await this.pause(); this.logger.info("Resetting SyncClient's local state"); - this.syncEventQueue.clearAllState(); + await this.syncEventQueue.clearAllState(); await this.syncEventQueue.save(); this.resetInMemoryState(); this.hasFinishedOfflineSync = false; @@ -353,18 +358,14 @@ export class SyncClient { await this.settings.setSettings(value); } - public syncLocallyCreatedFile( - relativePath: RelativePath - ): void { + public syncLocallyCreatedFile(relativePath: RelativePath): void { this.checkIfDestroyed("syncLocallyCreatedFile"); this.fileChangeNotifier.notifyOfFileChange(relativePath); this.syncer.syncLocallyCreatedFile(relativePath); } - public syncLocallyDeletedFile( - relativePath: RelativePath - ): void { + public syncLocallyDeletedFile(relativePath: RelativePath): void { this.checkIfDestroyed("syncLocallyDeletedFile"); this.fileChangeNotifier.notifyOfFileChange(relativePath); @@ -387,13 +388,6 @@ export class SyncClient { }); } - public get hasPendingWork(): boolean { - return ( - this.syncEventQueue.pendingUpdateCount > 0 || - this.webSocketManager.hasOutstandingWork - ); - } - public getDocumentSyncingStatus( relativePath: RelativePath ): DocumentSyncStatus { diff --git a/frontend/sync-client/src/sync-operations/conflict-path.test.ts b/frontend/sync-client/src/sync-operations/conflict-path.test.ts index 7f7bf67c..b27f2a0e 100644 --- a/frontend/sync-client/src/sync-operations/conflict-path.test.ts +++ b/frontend/sync-client/src/sync-operations/conflict-path.test.ts @@ -31,10 +31,7 @@ describe("buildConflictFileName", () => { 0, "stem length must be a whole number of families" ); - assert.ok( - !stem.endsWith("‍"), - "stem must not end with a dangling ZWJ" - ); + assert.ok(!stem.endsWith("‍"), "stem must not end with a dangling ZWJ"); }); it("does not split a base character from its combining mark", () => { @@ -61,7 +58,10 @@ describe("buildConflictFileName", () => { describe("CONFLICT_PATH_REGEX", () => { it("does not misclassify user-authored names that start with `conflict-`", () => { - assert.strictEqual(CONFLICT_PATH_REGEX.test("conflict-resolution.md"), false); + assert.strictEqual( + CONFLICT_PATH_REGEX.test("conflict-resolution.md"), + false + ); }); it("only inspects the final path segment", () => { @@ -80,6 +80,9 @@ describe("CONFLICT_PATH_REGEX", () => { }); it("round-trips with buildConflictFileName", () => { - assert.strictEqual(CONFLICT_PATH_REGEX.test(buildConflictFileName("note.md")), true); + assert.strictEqual( + CONFLICT_PATH_REGEX.test(buildConflictFileName("note.md")), + true + ); }); }); diff --git a/frontend/sync-client/src/sync-operations/conflict-path.ts b/frontend/sync-client/src/sync-operations/conflict-path.ts index 69942750..adc1bea1 100644 --- a/frontend/sync-client/src/sync-operations/conflict-path.ts +++ b/frontend/sync-client/src/sync-operations/conflict-path.ts @@ -8,16 +8,10 @@ export const CONFLICT_PATH_REGEX = /(?:^|\/)conflict-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[^/]*$/u; - const CONFLICT_PREFIX_LEN = "conflict-".length + 36 + 1; const MAX_SEGMENT_BYTES = 255; const MAX_ORIGINAL_BYTES = MAX_SEGMENT_BYTES - CONFLICT_PREFIX_LEN - 4; -export function buildConflictFileName(fileName: string): string { - const safeName = truncateFileNameToByteLimit(fileName, MAX_ORIGINAL_BYTES); - return `conflict-${crypto.randomUUID()}-${safeName}`; -} - function truncateFileNameToByteLimit( fileName: string, maxBytes: number @@ -34,7 +28,9 @@ function truncateFileNameToByteLimit( const extensionBytes = encoder.encode(extension).byteLength; const stemBudget = Math.max(0, maxBytes - extensionBytes); - const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + const segmenter = new Intl.Segmenter(undefined, { + granularity: "grapheme" + }); let truncatedStem = ""; let usedBytes = 0; for (const { segment } of segmenter.segment(stem)) { @@ -45,3 +41,8 @@ function truncateFileNameToByteLimit( } return truncatedStem + extension; } + +export function buildConflictFileName(fileName: string): string { + const safeName = truncateFileNameToByteLimit(fileName, MAX_ORIGINAL_BYTES); + return `conflict-${crypto.randomUUID()}-${safeName}`; +} diff --git a/frontend/sync-client/src/sync-operations/cursor-tracker.ts b/frontend/sync-client/src/sync-operations/cursor-tracker.ts index 98548f73..a52fea99 100644 --- a/frontend/sync-client/src/sync-operations/cursor-tracker.ts +++ b/frontend/sync-client/src/sync-operations/cursor-tracker.ts @@ -35,7 +35,7 @@ export class CursorTracker { []; public constructor( - private readonly logger: Logger, + logger: Logger, private readonly queue: SyncEventQueue, private readonly webSocketManager: WebSocketManager, private readonly fileOperations: FileOperations, @@ -82,8 +82,7 @@ export class CursorTracker { for (const clientCursor of this.knownRemoteCursors) { if ( clientCursor.documentsWithCursors.some( - (document) => - document.relativePath === relativePath + (document) => document.relativePath === relativePath ) ) { clientCursor.upToDateness = @@ -135,7 +134,9 @@ export class CursorTracker { const readContent = await this.fileOperations.read( doc.relativePath ); - const record = this.queue.getSettledDocumentByPath(doc.relativePath); + const record = this.queue.getSettledDocumentByPath( + doc.relativePath + ); if (record?.remoteHash !== (await hash(readContent))) { doc.vaultUpdateId = null; } @@ -221,20 +222,18 @@ export class CursorTracker { private async getDocumentUpToDateness( document: DocumentWithCursors ): Promise { - const record = this.queue.getSettledDocumentByPath(document.relativePath); + const record = this.queue.getSettledDocumentByPath( + document.relativePath + ); if (!record) { // the document of the cursor must be from the future return DocumentUpToDateness.Later; } - if ( - record.parentVersionId < (document.vaultUpdateId ?? 0) - ) { + if (record.parentVersionId < (document.vaultUpdateId ?? 0)) { return DocumentUpToDateness.Later; - } else if ( - (document.vaultUpdateId ?? 0) < record.parentVersionId - ) { + } else if ((document.vaultUpdateId ?? 0) < record.parentVersionId) { // the document of the cursor must be from the past return DocumentUpToDateness.Prior; } @@ -243,7 +242,9 @@ export class CursorTracker { document.relativePath ); - const currentRecord = this.queue.getSettledDocumentByPath(document.relativePath); + const currentRecord = this.queue.getSettledDocumentByPath( + document.relativePath + ); return currentRecord?.remoteHash === (await hash(currentContent)) ? DocumentUpToDateness.UpToDate : DocumentUpToDateness.Prior; diff --git a/frontend/sync-client/src/sync-operations/offline-change-detector.ts b/frontend/sync-client/src/sync-operations/offline-change-detector.ts index 8bb8c27c..1c07ef42 100644 --- a/frontend/sync-client/src/sync-operations/offline-change-detector.ts +++ b/frontend/sync-client/src/sync-operations/offline-change-detector.ts @@ -8,8 +8,6 @@ import { FileNotFoundError } from "../errors/file-not-found-error"; import type { SyncEventQueue } from "./sync-event-queue"; import { removeFromArray } from "../utils/remove-from-array"; - - /** * Scans the local filesystem and the document database to determine * which files were created, updated, moved, or deleted while the @@ -20,8 +18,11 @@ export async function scheduleOfflineChanges( operations: FileOperations, queue: SyncEventQueue, enqueueCreate: (path: RelativePath) => void, - enqueueUpdate: (args: { oldPath?: RelativePath; relativePath: RelativePath }) => void, - enqueueDelete: (path: RelativePath) => void, + enqueueUpdate: (args: { + oldPath?: RelativePath; + relativePath: RelativePath; + }) => void, + enqueueDelete: (path: RelativePath) => void ): Promise { const allLocalFiles = await operations.listFilesRecursively(); logger.info(`Scheduling sync for ${allLocalFiles.length} local files`); @@ -30,19 +31,14 @@ export async function scheduleOfflineChanges( const locallyPossiblyDeletedFiles: DocumentWithPath[] = []; for (const [path, record] of allDocuments.entries()) { - if ( - record !== undefined - ) { - locallyPossiblyDeletedFiles.push({ path, record }); - } + locallyPossiblyDeletedFiles.push({ path, record }); } const locallyPossibleCreatedFiles: RelativePath[] = []; const syncedLocalFiles: RelativePath[] = []; for (const localFile of allLocalFiles) { - if (allDocuments.has(localFile) - ) { + if (allDocuments.has(localFile)) { syncedLocalFiles.push(localFile); } else { locallyPossibleCreatedFiles.push(localFile); @@ -53,19 +49,27 @@ export async function scheduleOfflineChanges( const content = await operations.read(path); const contentHash = await hash(content); - const matchingDeletedFile = await findMatchingFile(contentHash, locallyPossiblyDeletedFiles); + const matchingDeletedFile = await findMatchingFile( + contentHash, + locallyPossiblyDeletedFiles + ); if (matchingDeletedFile !== undefined) { logger.debug( - `File ${path} might have been moved from ${matchingDeletedFile.path} while offline, scheduling sync to move it`, + `File ${path} might have been moved from ${matchingDeletedFile.path} while offline, scheduling sync to move it` ); - enqueueUpdate({ oldPath: matchingDeletedFile.path, relativePath: path }); + enqueueUpdate({ + oldPath: matchingDeletedFile.path, + relativePath: path + }); removeFromArray(locallyPossiblyDeletedFiles, matchingDeletedFile); removeFromArray(locallyPossibleCreatedFiles, path); } } for (const path of locallyPossibleCreatedFiles) { - logger.debug(`File ${path} was created while offline, scheduling sync to create it`); + logger.debug( + `File ${path} was created while offline, scheduling sync to create it` + ); enqueueCreate(path); } diff --git a/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts b/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts index 99b37bf8..c1dbce9e 100644 --- a/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts +++ b/frontend/sync-client/src/sync-operations/sync-event-queue.test.ts @@ -9,8 +9,12 @@ import type { DocumentRecord, RelativePath } from "./types"; function createQueue(ignorePatterns: string[] = []): SyncEventQueue { const logger = new Logger(); - const settings = new Settings(logger, { ignorePatterns }, async () => { }); - return new SyncEventQueue(settings, logger, undefined, async () => { }); + const settings = new Settings(logger, { ignorePatterns }, async () => { + /* no-op */ + }); + return new SyncEventQueue(settings, logger, undefined, async () => { + /* no-op */ + }); } function fakeRemoteVersion( @@ -60,9 +64,7 @@ describe("SyncEventQueue", () => { const third = await queue.next(); assert.strictEqual(third?.type, SyncEventType.LocalDelete); - if (third?.type === SyncEventType.LocalDelete) { - assert.strictEqual(third.documentId, "A"); - } + assert.strictEqual(third.documentId, "A"); assert.strictEqual(await queue.next(), undefined); }); @@ -74,15 +76,11 @@ describe("SyncEventQueue", () => { const first = await queue.next(); assert.strictEqual(first?.type, SyncEventType.LocalCreate); - if (first?.type === SyncEventType.LocalCreate) { - assert.strictEqual(first.path, "a.md"); - } + assert.strictEqual(first.path, "a.md"); const second = await queue.next(); assert.strictEqual(second?.type, SyncEventType.LocalCreate); - if (second?.type === SyncEventType.LocalCreate) { - assert.strictEqual(second.path, "b.md"); - } + assert.strictEqual(second.path, "b.md"); }); it("delete resolves documentId from path", async () => { @@ -93,14 +91,15 @@ describe("SyncEventQueue", () => { const event = await queue.next(); assert.strictEqual(event?.type, SyncEventType.LocalDelete); - if (event?.type === SyncEventType.LocalDelete) { - assert.strictEqual(event.documentId, "A"); - } + assert.strictEqual(event.documentId, "A"); }); it("delete for unknown path is silently ignored", async () => { const queue = createQueue(); - await queue.enqueue({ type: SyncEventType.LocalDelete, path: "unknown.md" }); + await queue.enqueue({ + type: SyncEventType.LocalDelete, + path: "unknown.md" + }); assert.strictEqual(queue.pendingUpdateCount, 0); }); @@ -112,11 +111,14 @@ describe("SyncEventQueue", () => { await queue.setDocument("a.md", fakeRecord("A")); assert.strictEqual(queue.syncedDocumentCount, 1); - assert.deepStrictEqual(queue.getSettledDocumentByPath("a.md"), fakeRecord("A")); + assert.deepStrictEqual( + queue.getSettledDocumentByPath("a.md"), + fakeRecord("A") + ); const found = queue.getDocumentByDocumentId("A"); assert.strictEqual(found?.path, "a.md"); - assert.strictEqual(found?.record.documentId, "A"); + assert.strictEqual(found.record.documentId, "A"); await queue.removeDocument("a.md"); assert.strictEqual(queue.syncedDocumentCount, 0); @@ -127,9 +129,16 @@ describe("SyncEventQueue", () => { const queue = createQueue(); await queue.setDocument("a.md", fakeRecord("A")); - await queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md", oldPath: "a.md" }); + await queue.enqueue({ + type: SyncEventType.LocalUpdate, + path: "b.md", + oldPath: "a.md" + }); assert.strictEqual(queue.getSettledDocumentByPath("a.md"), undefined); - assert.strictEqual(queue.getSettledDocumentByPath("b.md")?.documentId, "A"); + assert.strictEqual( + queue.getSettledDocumentByPath("b.md")?.documentId, + "A" + ); }); it("create can be re-enqueued after being dequeued", async () => { @@ -144,11 +153,20 @@ describe("SyncEventQueue", () => { it("silently ignores create events matching ignore patterns", async () => { const queue = createQueue(["*.tmp", ".hidden/**"]); - await queue.enqueue({ type: SyncEventType.LocalCreate, path: "scratch.tmp" }); - await queue.enqueue({ type: SyncEventType.LocalCreate, path: ".hidden/secret.md" }); + await queue.enqueue({ + type: SyncEventType.LocalCreate, + path: "scratch.tmp" + }); + await queue.enqueue({ + type: SyncEventType.LocalCreate, + path: ".hidden/secret.md" + }); assert.strictEqual(queue.pendingUpdateCount, 0); - await queue.enqueue({ type: SyncEventType.LocalCreate, path: "notes-new.md" }); + await queue.enqueue({ + type: SyncEventType.LocalCreate, + path: "notes-new.md" + }); assert.strictEqual(queue.pendingUpdateCount, 1); await queue.enqueue({ @@ -170,7 +188,10 @@ describe("SyncEventQueue", () => { assert.strictEqual(queue.pendingUpdateCount, 0); assert.strictEqual(queue.syncedDocumentCount, 1); - assert.strictEqual(queue.getSettledDocumentByPath("a.md")?.documentId, "A"); + assert.strictEqual( + queue.getSettledDocumentByPath("a.md")?.documentId, + "A" + ); }); it("allSettledDocuments returns all tracked documents", async () => { @@ -186,24 +207,39 @@ describe("SyncEventQueue", () => { it("loads initial state from persistence", () => { const logger = new Logger(); - const settings = new Settings(logger, {}, async () => { }); - const queue = new SyncEventQueue(settings, logger, { - documents: [ - { - relativePath: "a.md", - ...fakeRecord("A", { parentVersionId: 5 }) - }, - { - relativePath: "b.md", - ...fakeRecord("B", { parentVersionId: 3 }) - } - ], - lastSeenUpdateId: 4 - }, async () => { }); + const settings = new Settings(logger, {}, async () => { + /* no-op */ + }); + const queue = new SyncEventQueue( + settings, + logger, + { + documents: [ + { + relativePath: "a.md", + ...fakeRecord("A", { parentVersionId: 5 }) + }, + { + relativePath: "b.md", + ...fakeRecord("B", { parentVersionId: 3 }) + } + ], + lastSeenUpdateId: 4 + }, + async () => { + /* no-op */ + } + ); assert.strictEqual(queue.syncedDocumentCount, 2); - assert.strictEqual(queue.getSettledDocumentByPath("a.md")?.documentId, "A"); - assert.strictEqual(queue.getSettledDocumentByPath("b.md")?.documentId, "B"); + assert.strictEqual( + queue.getSettledDocumentByPath("a.md")?.documentId, + "A" + ); + assert.strictEqual( + queue.getSettledDocumentByPath("b.md")?.documentId, + "B" + ); assert.strictEqual(queue.lastSeenUpdateId, 4); }); @@ -216,10 +252,16 @@ describe("SyncEventQueue", () => { assert.ok(event?.type === SyncEventType.LocalCreate); const createPromise = event.resolvers.promise; - await queue.resolveCreate(event, fakeRecord("DOC-1", { parentVersionId: 5 })); + await queue.resolveCreate( + event, + fakeRecord("DOC-1", { parentVersionId: 5 }) + ); // Document is now settled - assert.strictEqual(queue.getSettledDocumentByPath("a.md")?.documentId, "DOC-1"); + assert.strictEqual( + queue.getSettledDocumentByPath("a.md")?.documentId, + "DOC-1" + ); // Promise was resolved assert.strictEqual(await createPromise, "DOC-1"); diff --git a/frontend/sync-client/src/sync-operations/sync-event-queue.ts b/frontend/sync-client/src/sync-operations/sync-event-queue.ts index 69856e8d..66ddf7eb 100644 --- a/frontend/sync-client/src/sync-operations/sync-event-queue.ts +++ b/frontend/sync-client/src/sync-operations/sync-event-queue.ts @@ -3,8 +3,8 @@ import type { Logger } from "../tracing/logger"; import { globsToRegexes } from "../utils/globs-to-regexes"; import { CONFLICT_PATH_REGEX } from "./conflict-path"; import { removeFromArray } from "../utils/remove-from-array"; +import type { DocumentWithPath } from "./types"; import { - DocumentWithPath, SyncEventType, type DocumentId, type DocumentRecord, @@ -12,27 +12,28 @@ import { type RelativePath, type StoredSyncState, type SyncEvent, - type VaultUpdateId, + type VaultUpdateId } from "./types"; import { MinCovered } from "../utils/data-structures/min-covered"; - export class SyncEventQueue { + private _lastSeenUpdateId: MinCovered; + // Latest state of the filesystem as we know it, excluding // unconfirmed creates but including pending deletes. // // It's always indexed by the latest path on disk. - // + // // It maps a subset of the remote state onto the local filesystem. private readonly documents = new Map(); // All outstanding operations in order of occurrence, - // can include multiple generations of the same document, + // can include multiple generations of the same document, // e.g.: a create, delete, create sequence for the same path. // // The paths within the events must always correspond to the latest // path on disk, so the path of each event may be updated multiple - // times. + // times. // // It maps pending changes onto the local filesystem. private readonly events: SyncEvent[] = []; @@ -40,8 +41,6 @@ export class SyncEventQueue { // file creations for paths matching any of these patterns will be ignored private ignorePatterns: RegExp[]; - public _lastSeenUpdateId: MinCovered; - public constructor( private readonly settings: Settings, private readonly logger: Logger, @@ -70,17 +69,13 @@ export class SyncEventQueue { this.documents.set(relativePath, record); } } - this._lastSeenUpdateId = new MinCovered(initialState.lastSeenUpdateId ?? 0); + this._lastSeenUpdateId = new MinCovered( + initialState.lastSeenUpdateId ?? 0 + ); - this.logger.debug(`Loaded ${this.documents.size} documents and lastSeenUpdateId=${this._lastSeenUpdateId} from storage`); - } - - public get lastSeenUpdateId(): VaultUpdateId { - return this._lastSeenUpdateId.min; - } - - public set lastSeenUpdateId(id: VaultUpdateId) { - this._lastSeenUpdateId.add(id); + this.logger.debug( + `Loaded ${this.documents.size} documents and lastSeenUpdateId=${this._lastSeenUpdateId.min} from storage` + ); } public get pendingUpdateCount(): number { @@ -91,8 +86,19 @@ export class SyncEventQueue { return this.documents.size; } + public get lastSeenUpdateId(): VaultUpdateId { + return this._lastSeenUpdateId.min; + } + + public set lastSeenUpdateId(id: VaultUpdateId) { + this._lastSeenUpdateId.add(id); + } + public async enqueue(input: FileSyncEvent): Promise { - const path = (input.type === SyncEventType.RemoteChange) ? input.remoteVersion.relativePath : input.path; + const path = + input.type === SyncEventType.RemoteChange + ? input.remoteVersion.relativePath + : input.path; if (this.ignorePatterns.some((pattern) => pattern.test(path))) { this.logger.info( @@ -106,22 +112,28 @@ export class SyncEventQueue { return; } - if (input.type === SyncEventType.LocalCreate) { - this.events.push({ type: SyncEventType.LocalCreate, path, originalPath: path, resolvers: Promise.withResolvers() }); + this.events.push({ + type: SyncEventType.LocalCreate, + path, + originalPath: path, + resolvers: Promise.withResolvers() + }); return; } - const lookupPath = (input.type === SyncEventType.LocalUpdate && input.oldPath) ? input.oldPath : path; + const lookupPath = + input.type === SyncEventType.LocalUpdate && + input.oldPath !== undefined + ? input.oldPath + : path; const record = this.documents.get(lookupPath); // latest creation must take precedence as it's from the doc's latest generation const pendingDocumentId: Promise | undefined = this.findLatestCreateForPath(lookupPath)?.resolvers.promise; - const documentId: DocumentId | undefined = - record?.documentId; - + const documentId: DocumentId | undefined = record?.documentId; if (pendingDocumentId === undefined && documentId === undefined) { // we can get here when deleting a local document after a remote update @@ -129,7 +141,14 @@ export class SyncEventQueue { } if (input.type === SyncEventType.LocalDelete) { - this.events.push({ type: SyncEventType.LocalDelete, documentId: pendingDocumentId ?? documentId! }); + const deleteId = pendingDocumentId ?? documentId; + if (deleteId === undefined) { + throw new Error("Unreachable: deleteId must be defined here"); + } + this.events.push({ + type: SyncEventType.LocalDelete, + documentId: deleteId + }); return; } @@ -137,30 +156,43 @@ export class SyncEventQueue { if (pendingDocumentId !== undefined) { this.updatePendingCreatePath(input.oldPath, path); } else { + if (record === undefined) { + throw new Error( + "Unreachable: record must be defined for non-pending update" + ); + } this.documents.delete(input.oldPath); - this.documents.set(path, record!); + this.documents.set(path, record); for (const e of this.events) { - // It already has a docId, so there can't be a pending create event for it - if (e.type === SyncEventType.LocalUpdate && e.documentId === documentId) { + // It already has a docId, so there can't be a pending create event for it + if ( + e.type === SyncEventType.LocalUpdate && + e.documentId === documentId + ) { e.path = path; } } await this.save(); - } - return + return; } - this.events.push({ type: SyncEventType.LocalUpdate, documentId: pendingDocumentId ?? documentId!, path, originalPath: path }); + const updateId = pendingDocumentId ?? documentId; + if (updateId === undefined) { + throw new Error("Unreachable: updateId must be defined here"); + } + this.events.push({ + type: SyncEventType.LocalUpdate, + documentId: updateId, + path, + originalPath: path + }); } - - public async next(): Promise { return this.events.shift(); } - /** * Call once a create has been acknowledged by the server. */ @@ -170,19 +202,21 @@ export class SyncEventQueue { ): Promise { removeFromArray(this.events, event); // in case the create event is still pending await this.setDocument(event.path, record); - event.resolvers?.resolve(record.documentId); + event.resolvers.resolve(record.documentId); } /** * Update the settled document map and persist the new document version. */ - public setDocument(path: RelativePath, record: DocumentRecord): Promise { + public async setDocument( + path: RelativePath, + record: DocumentRecord + ): Promise { this.documents.set(path, record); return this.save(); - } - public removeDocument(path: RelativePath): Promise { + public async removeDocument(path: RelativePath): Promise { this.documents.delete(path); return this.save(); } @@ -198,11 +232,7 @@ export class SyncEventQueue { return undefined; } - - - public getDocumentByDocumentIdOrFail( - target: DocumentId - ): DocumentWithPath { + public getDocumentByDocumentIdOrFail(target: DocumentId): DocumentWithPath { const result = this.getDocumentByDocumentId(target); if (!result) { throw new Error(`No document found with id ${target}`); @@ -210,10 +240,6 @@ export class SyncEventQueue { return result; } - - - - public async save(): Promise { return this.saveData({ documents: Array.from(this.documents.entries()).map( @@ -227,16 +253,16 @@ export class SyncEventQueue { } // todo: let's remove - public getSettledDocumentByPath(path: RelativePath): DocumentRecord | undefined { + public getSettledDocumentByPath( + path: RelativePath + ): DocumentRecord | undefined { return this.documents.get(path); } - public allSettledDocuments(): Map { return new Map(this.documents.entries()); } - public hasPendingEventsForPath(path: RelativePath): boolean { const record = this.documents.get(path); if (record === undefined) { @@ -252,7 +278,8 @@ export class SyncEventQueue { e.documentId === docId) || (e.type === SyncEventType.RemoteChange && // we care about the local path not the remote - this.getDocumentByDocumentId(e.remoteVersion.documentId)?.path === path) + this.getDocumentByDocumentId(e.remoteVersion.documentId) + ?.path === path) ); } @@ -266,11 +293,10 @@ export class SyncEventQueue { ); } - public async clearAllState(): Promise { this.clearPending(); this.documents.clear(); - this._lastSeenUpdateId.reset() + this._lastSeenUpdateId.reset(); await this.save(); } @@ -279,29 +305,6 @@ export class SyncEventQueue { this.events.length = 0; } - - private updatePendingCreatePath( - oldPath: RelativePath, - newPath: RelativePath - ): void { - const createEvent = this.findLatestCreateForPath(oldPath); - if (createEvent === undefined) return; - - const promise = createEvent.resolvers?.promise; - createEvent.path = newPath; - - if (promise !== undefined) { - for (const e of this.events) { - if ( - e.type === SyncEventType.LocalUpdate && - e.documentId === promise - ) { - e.path = newPath; - } - } - } - } - public findLatestCreateForPath( path: RelativePath ): Extract | undefined { @@ -314,18 +317,34 @@ export class SyncEventQueue { return undefined; } + private updatePendingCreatePath( + oldPath: RelativePath, + newPath: RelativePath + ): void { + const createEvent = this.findLatestCreateForPath(oldPath); + if (createEvent === undefined) return; + const { promise } = createEvent.resolvers; + createEvent.path = newPath; - - - private rejectAllPendingCreates(): void { - for (const event of this.events) { - if (event.type === SyncEventType.LocalCreate && event.resolvers !== undefined) { - event.resolvers.promise.catch(() => { /* suppressed — consumer may not be listening */ }); - event.resolvers.reject(new Error("Create was cancelled")); + for (const e of this.events) { + if ( + e.type === SyncEventType.LocalUpdate && + e.documentId === promise + ) { + e.path = newPath; } } } - + private rejectAllPendingCreates(): void { + for (const event of this.events) { + if (event.type === SyncEventType.LocalCreate) { + event.resolvers.promise.catch(() => { + /* suppressed — consumer may not be listening */ + }); + event.resolvers.reject(new Error("Create was cancelled")); + } + } + } } diff --git a/frontend/sync-client/src/sync-operations/syncer.ts b/frontend/sync-client/src/sync-operations/syncer.ts index c0334dbf..298c35a4 100644 --- a/frontend/sync-client/src/sync-operations/syncer.ts +++ b/frontend/sync-client/src/sync-operations/syncer.ts @@ -4,12 +4,15 @@ import { type DocumentRecord, type SyncEvent, type RelativePath, - type VaultUpdateId, + type VaultUpdateId } from "./types"; import type { Logger } from "../tracing/logger"; import { hash } from "../utils/hash"; import type { Settings } from "../persistence/settings"; -import { MoveOnConflict, type FileOperations } from "../file-operations/file-operations"; +import { + MoveOnConflict, + type FileOperations +} from "../file-operations/file-operations"; import { scheduleOfflineChanges } from "./offline-change-detector"; import { SyncResetError } from "../errors/sync-reset-error"; import type { DocumentVersionWithoutContent } from "../services/types/DocumentVersionWithoutContent"; @@ -21,9 +24,7 @@ import type { SyncEventQueue } from "./sync-event-queue"; import type { SyncService } from "../services/sync-service"; import { FileNotFoundError } from "../errors/file-not-found-error"; import { HttpClientError } from "../errors/http-client-error"; -import type { - SyncHistory -} from "../tracing/sync-history"; +import type { SyncHistory } from "../tracing/sync-history"; import { SyncStatus, SyncType, @@ -79,7 +80,10 @@ export class Syncer { } public syncLocallyCreatedFile(relativePath: RelativePath): void { - void this.queue.enqueue({ type: SyncEventType.LocalCreate, path: relativePath }); + void this.queue.enqueue({ + type: SyncEventType.LocalCreate, + path: relativePath + }); this.ensureDraining(); } @@ -90,14 +94,18 @@ export class Syncer { oldPath?: RelativePath; relativePath: RelativePath; }): void { - void this.queue.enqueue({ type: SyncEventType.LocalUpdate, path: relativePath, oldPath }); + void this.queue.enqueue({ + type: SyncEventType.LocalUpdate, + path: relativePath, + oldPath + }); this.ensureDraining(); } public syncLocallyDeletedFile(relativePath: RelativePath): void { void this.queue.enqueue({ type: SyncEventType.LocalDelete, - path: relativePath, + path: relativePath }); this.ensureDraining(); } @@ -151,7 +159,6 @@ export class Syncer { } } - public reset(): void { this._isFirstSyncStarted = false; this.queue.clearPending(); @@ -162,20 +169,14 @@ export class Syncer { // fresh scan can only start once the prior one is done. const current = this.runningScheduleSyncForOfflineChanges; if (current !== undefined) { - current.finally(() => { - if ( - this.runningScheduleSyncForOfflineChanges === - current - ) { - this.runningScheduleSyncForOfflineChanges = - undefined; + void current.finally(() => { + if (this.runningScheduleSyncForOfflineChanges === current) { + this.runningScheduleSyncForOfflineChanges = undefined; } }); } } - - private sendHandshakeMessage(): void { const message: WebSocketClientMessage = { type: "handshake", @@ -186,8 +187,6 @@ export class Syncer { this.webSocketManager.sendHandshakeMessage(message); } - - private async internalScheduleSyncForOfflineChanges(): Promise { this.isScanning = true; try { @@ -195,10 +194,18 @@ export class Syncer { await this.drainPromise; } await scheduleOfflineChanges( - this.logger, this.operations, this.queue, - (path) => { this.syncLocallyCreatedFile(path); }, - (args) => { this.syncLocallyUpdatedFile(args); }, - (path) => { this.syncLocallyDeletedFile(path); }, + this.logger, + this.operations, + this.queue, + (path) => { + this.syncLocallyCreatedFile(path); + }, + (args) => { + this.syncLocallyUpdatedFile(args); + }, + (path) => { + this.syncLocallyDeletedFile(path); + } ); } finally { this.isScanning = false; @@ -207,9 +214,6 @@ export class Syncer { this.ensureDraining(); } - - - private ensureDraining(): void { if (this.drainPromise !== undefined) return; if (this.isScanning) return; @@ -218,7 +222,6 @@ export class Syncer { }); } - private async drain(): Promise { let event = await this.queue.next(); while (event !== undefined) { @@ -271,8 +274,10 @@ export class Syncer { `Skipping sync event '${event.type}' because the file no longer exists` ); if (event.type === SyncEventType.LocalCreate) { - event.resolvers?.promise.catch(() => { }); - event.resolvers?.reject(new Error("Create was cancelled")); + event.resolvers.promise.catch(() => { + /* suppressed */ + }); + event.resolvers.reject(new Error("Create was cancelled")); } return; } @@ -285,10 +290,10 @@ export class Syncer { // promise would otherwise hang forever, blocking any // queued Delete / SyncLocal that `await`s it. if (event.type === SyncEventType.LocalCreate) { - event.resolvers?.promise.catch(() => { + event.resolvers.promise.catch(() => { /* suppressed */ }); - event.resolvers?.reject( + event.resolvers.reject( new Error( `Create was cancelled — server rejected the request (${e.message})` ) @@ -300,10 +305,9 @@ export class Syncer { } } - private async skipIfOversized(event: SyncEvent): Promise { - let sizeInBytes: number; - let relativePath: RelativePath; + let sizeInBytes = 0; + let relativePath: RelativePath = ""; switch (event.type) { case SyncEventType.LocalDelete: @@ -316,7 +320,7 @@ export class Syncer { case SyncEventType.RemoteChange: if (event.remoteVersion.isDeleted) return false; sizeInBytes = event.remoteVersion.contentSize; - relativePath = event.remoteVersion.relativePath; + ({ relativePath } = event.remoteVersion); break; } @@ -329,8 +333,10 @@ export class Syncer { this.history.addHistoryEntry(oversizedEntry); if (event.type === SyncEventType.LocalCreate) { - event.resolvers?.promise.catch(() => { }); - event.resolvers?.reject(new Error("Create was cancelled")); + event.resolvers.promise.catch(() => { + /* suppressed */ + }); + event.resolvers.reject(new Error("Create was cancelled")); } return true; @@ -354,9 +360,6 @@ export class Syncer { } } - - - private async processCreate( event: Extract ): Promise { @@ -378,13 +381,13 @@ export class Syncer { createEvent: event }); - this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, details: { type: SyncType.CREATE, relativePath: effectivePath }, - message: response.type === "MergingUpdate" - ? "Created file and merged with existing remote version" - : "Successfully created file on the server", + message: + response.type === "MergingUpdate" + ? "Created file and merged with existing remote version" + : "Successfully created file on the server", author: response.userId, timestamp: new Date(response.updatedDate) }); @@ -393,7 +396,7 @@ export class Syncer { private async processDelete( event: Extract ): Promise { - let documentId = await event.documentId; + const documentId = await event.documentId; const doc = this.queue.getDocumentByDocumentIdOrFail(documentId); const relativePath = doc.path; @@ -406,7 +409,6 @@ export class Syncer { await this.queue.removeDocument(doc.path); this.queue.lastSeenUpdateId = response.vaultUpdateId; - this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, details: { @@ -421,16 +423,16 @@ export class Syncer { private async processLocalUpdate( event: Extract ): Promise { - let documentId = await event.documentId; + const documentId = await event.documentId; - const { path: diskPath, record } = this.queue.getDocumentByDocumentIdOrFail(documentId); + const { path: diskPath, record } = + this.queue.getDocumentByDocumentIdOrFail(documentId); const contentBytes = await this.operations.read(diskPath); const contentHash = await hash(contentBytes); const hashChanged = contentHash !== record.remoteHash; - const pathChanged = - record.remoteRelativePath !== event.originalPath; + const pathChanged = record.remoteRelativePath !== event.originalPath; if (!hashChanged && !pathChanged) { this.logger.debug( @@ -443,12 +445,10 @@ export class Syncer { record, relativePath: event.originalPath, contentBytes - } - ); + }); this.queue.lastSeenUpdateId = response.vaultUpdateId; - await this.handleMaybeMergingResponse({ path: diskPath, response, @@ -456,9 +456,7 @@ export class Syncer { originalContentBytes: contentBytes }); - - const isMerge = - "type" in response && response.type === "MergingUpdate"; + const isMerge = "type" in response && response.type === "MergingUpdate"; this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, details: { @@ -489,12 +487,12 @@ export class Syncer { // response) createEvent?: Extract; }): Promise { - let record = { + const record = { documentId: response.documentId, parentVersionId: response.vaultUpdateId, remoteRelativePath: response.relativePath }; - let remoteHash: string; + let remoteHash = ""; if ("type" in response && response.type === "MergingUpdate") { const responseBytes = base64ToBytes(response.contentBase64); @@ -506,11 +504,7 @@ export class Syncer { remoteHash = await hash(responseBytes); - await this.updateCache( - response.vaultUpdateId, - responseBytes, - path - ); + await this.updateCache(response.vaultUpdateId, responseBytes, path); } else { // Fast-forward update: no merge needed remoteHash = contentHash; @@ -524,13 +518,16 @@ export class Syncer { if (createEvent === undefined) { // a http response will always be more up-to-date than any queued remote update - this.operations.move(path, response.relativePath, MoveOnConflict.EXISTING); + await this.operations.move( + path, + response.relativePath, + MoveOnConflict.EXISTING + ); await this.queue.setDocument(response.relativePath, { ...record, remoteHash }); - } else { // The response to a create must contain the path from the create request await this.queue.resolveCreate(createEvent, { @@ -542,7 +539,6 @@ export class Syncer { this.queue.lastSeenUpdateId = response.vaultUpdateId; } - private async processRemoteChange( event: Extract ): Promise { @@ -556,10 +552,16 @@ export class Syncer { // trying to delete a document we've already scheduled for deletion locally return; } - return this.processRemoteDelete(documentWithPath.path, remoteVersion); + return this.processRemoteDelete( + documentWithPath.path, + remoteVersion + ); } - if (documentWithPath?.record.parentVersionId ?? 0 >= remoteVersion.vaultUpdateId) { + if ( + (documentWithPath?.record.parentVersionId ?? 0) >= + remoteVersion.vaultUpdateId + ) { this.queue.lastSeenUpdateId = remoteVersion.vaultUpdateId; this.logger.debug( `Document ${remoteVersion.relativePath} is already up-to-date or has newer local changes; skipping remote update` @@ -569,26 +571,36 @@ export class Syncer { if (documentWithPath !== undefined) { // must be the update to an existing doc - return this.processRemoteUpdate(documentWithPath.path, documentWithPath.record, remoteVersion); + return this.processRemoteUpdate( + documentWithPath.path, + documentWithPath.record, + remoteVersion + ); } - const pendingCreate = this.queue.findLatestCreateForPath(remoteVersion.relativePath); + const pendingCreate = this.queue.findLatestCreateForPath( + remoteVersion.relativePath + ); if (pendingCreate === undefined) { return this.processRemoteCreateForNewDocument(remoteVersion); } else { - return this.processRemoteCreateForPendingDocument(remoteVersion, pendingCreate); + return this.processRemoteCreateForPendingDocument( + remoteVersion, + pendingCreate + ); } } - - private async processRemoteDelete(path: RelativePath, remoteVersion: DocumentVersionWithoutContent): Promise { + private async processRemoteDelete( + path: RelativePath, + remoteVersion: DocumentVersionWithoutContent + ): Promise { await this.operations.delete(path); await this.queue.removeDocument(path); this.queue.lastSeenUpdateId = remoteVersion.vaultUpdateId; - this.history.addHistoryEntry({ status: SyncStatus.SUCCESS, details: { @@ -602,22 +614,29 @@ export class Syncer { }); } - private async processRemoteUpdate(path: RelativePath, record: DocumentRecord, remoteVersion: DocumentVersionWithoutContent): Promise { - if ( - record.parentVersionId >= - remoteVersion.vaultUpdateId - ) { - this.logger.debug( - `Document ${path} is already up-to-date` - ); + private async processRemoteUpdate( + path: RelativePath, + record: DocumentRecord, + remoteVersion: DocumentVersionWithoutContent + ): Promise { + if (record.parentVersionId >= remoteVersion.vaultUpdateId) { + this.logger.debug(`Document ${path} is already up-to-date`); return; } - if (!this.queue.hasPendingLocalEventsForDocumentId(remoteVersion.documentId)) { + if ( + !this.queue.hasPendingLocalEventsForDocumentId( + remoteVersion.documentId + ) + ) { // no local changes const currentContent = await this.operations.read(path); - const remoteContent = await this.syncService.getDocumentVersionContent({ documentId: remoteVersion.documentId, vaultUpdateId: remoteVersion.vaultUpdateId }); - this.operations.write(path, currentContent, remoteContent); + const remoteContent = + await this.syncService.getDocumentVersionContent({ + documentId: remoteVersion.documentId, + vaultUpdateId: remoteVersion.vaultUpdateId + }); + await this.operations.write(path, currentContent, remoteContent); await this.updateCache( remoteVersion.vaultUpdateId, @@ -625,20 +644,26 @@ export class Syncer { path ); this.queue.lastSeenUpdateId = remoteVersion.vaultUpdateId; + } // else we don't need to update the content, a subsequent local update will do that - } // else we don't need to update the content, a subsequent local update will do that - - this.syncRemotelyUpdatedFile({ // schedule it so that the lastSeenUpdateId remains consistent - document: - remoteVersion - }) - + void this.syncRemotelyUpdatedFile({ + // schedule it so that the lastSeenUpdateId remains consistent + document: remoteVersion + }); // wait for a local edit to do the actual updating here, so we can't even update the lastSeenUpdateId here - const conflictingDoc = this.queue.getSettledDocumentByPath(remoteVersion.relativePath); - const actualRelativePath = await this.operations.move(path, remoteVersion.relativePath, conflictingDoc?.parentVersionId ?? 0 < remoteVersion.vaultUpdateId ? MoveOnConflict.EXISTING : MoveOnConflict.NEW); + const conflictingDoc = this.queue.getSettledDocumentByPath( + remoteVersion.relativePath + ); + const actualRelativePath = await this.operations.move( + path, + remoteVersion.relativePath, + (conflictingDoc?.parentVersionId ?? 0) < remoteVersion.vaultUpdateId + ? MoveOnConflict.EXISTING + : MoveOnConflict.NEW + ); - this.queue.setDocument(actualRelativePath, { + await this.queue.setDocument(actualRelativePath, { ...record, remoteRelativePath: actualRelativePath }); @@ -651,22 +676,28 @@ export class Syncer { movedFrom: path }, // todo: eh - message: `File was renamed remotely from ${path} to ${actualRelativePath}`, + message: `File was renamed remotely from ${path} to ${actualRelativePath}` }); } - private async processRemoteCreateForNewDocument(remoteVersion: DocumentVersionWithoutContent): Promise { + private async processRemoteCreateForNewDocument( + remoteVersion: DocumentVersionWithoutContent + ): Promise { const remoteContent = await this.syncService.getDocumentVersionContent({ documentId: remoteVersion.documentId, vaultUpdateId: remoteVersion.vaultUpdateId }); - const conflictingDoc = this.queue.getSettledDocumentByPath(remoteVersion.relativePath); + const conflictingDoc = this.queue.getSettledDocumentByPath( + remoteVersion.relativePath + ); const actualPath = await this.operations.create( remoteVersion.relativePath, remoteContent, - conflictingDoc?.parentVersionId ?? 0 < remoteVersion.vaultUpdateId ? MoveOnConflict.EXISTING : MoveOnConflict.NEW + (conflictingDoc?.parentVersionId ?? 0) < remoteVersion.vaultUpdateId + ? MoveOnConflict.EXISTING + : MoveOnConflict.NEW ); await this.updateCache( @@ -703,7 +734,10 @@ export class Syncer { // We must avoid duplicating files. private async processRemoteCreateForPendingDocument( remoteVersion: DocumentVersionWithoutContent, - pendingCreateEvent: Extract + pendingCreateEvent: Extract< + SyncEvent, + { type: SyncEventType.LocalCreate } + > ): Promise { const remoteContent = await this.syncService.getDocumentVersionContent({ documentId: remoteVersion.documentId, @@ -712,7 +746,9 @@ export class Syncer { const remoteHash = await hash(remoteContent); const path = remoteVersion.relativePath; - const currentContent = await this.operations.read(pendingCreateEvent.path); + const currentContent = await this.operations.read( + pendingCreateEvent.path + ); await this.operations.write(path, currentContent, remoteContent); await this.updateCache( @@ -735,25 +771,21 @@ export class Syncer { type: SyncType.UPDATE, relativePath: path }, - message: - `Adopted remote create at ${path}`, + message: `Adopted remote create at ${path}`, author: remoteVersion.userId, timestamp: new Date(remoteVersion.updatedDate) }); - } - - - - - private async sendUpdate( - { record, relativePath, contentBytes }: { - record: DocumentRecord, - relativePath: RelativePath, - contentBytes: Uint8Array - } - ): Promise { + private async sendUpdate({ + record, + relativePath, + contentBytes + }: { + record: DocumentRecord; + relativePath: RelativePath; + contentBytes: Uint8Array; + }): Promise { const isText = !isBinary(contentBytes) && isFileTypeMergable( @@ -783,8 +815,6 @@ export class Syncer { }); } - - private async updateCache( updateId: VaultUpdateId, contentBytes: Uint8Array, diff --git a/frontend/sync-client/src/sync-operations/types.ts b/frontend/sync-client/src/sync-operations/types.ts index 9d2cedac..4cdac588 100644 --- a/frontend/sync-client/src/sync-operations/types.ts +++ b/frontend/sync-client/src/sync-operations/types.ts @@ -29,36 +29,41 @@ export enum SyncEventType { LocalCreate = "local-create", LocalUpdate = "local-update", // includes both content and path changes LocalDelete = "local-delete", - RemoteChange = "remote-change", // includes every type of create/update/delete coming from the server + RemoteChange = "remote-change" // includes every type of create/update/delete coming from the server } export type FileSyncEvent = | { type: SyncEventType.LocalCreate; path: RelativePath } | { - type: SyncEventType.LocalUpdate; path: RelativePath; oldPath?: RelativePath // oldPath is undefined for content changes - } + type: SyncEventType.LocalUpdate; + path: RelativePath; + oldPath?: RelativePath; // oldPath is undefined for content changes + } | { type: SyncEventType.LocalDelete; path: RelativePath } - | { type: SyncEventType.RemoteChange; remoteVersion: DocumentVersionWithoutContent }; + | { + type: SyncEventType.RemoteChange; + remoteVersion: DocumentVersionWithoutContent; + }; export type SyncEvent = | { - type: SyncEventType.LocalCreate; - path: RelativePath; // current path on disk - originalPath: RelativePath; // original path on disk when the event was queued - resolvers: PromiseWithResolvers - } + type: SyncEventType.LocalCreate; + path: RelativePath; // current path on disk + originalPath: RelativePath; // original path on disk when the event was queued + resolvers: PromiseWithResolvers; + } | { - type: SyncEventType.LocalUpdate; - documentId: DocumentId | Promise; // if it's a promise, the promise is fulfilled once the document's create event is processed - path: RelativePath; // current path on disk - originalPath: RelativePath; // original path on disk when the event was queued - // no need to store the old path in case of a rename; the server will figure it out from the parent's path - } + type: SyncEventType.LocalUpdate; + documentId: DocumentId | Promise; // if it's a promise, the promise is fulfilled once the document's create event is processed + path: RelativePath; // current path on disk + originalPath: RelativePath; // original path on disk when the event was queued + // no need to store the old path in case of a rename; the server will figure it out from the parent's path + } | { - type: SyncEventType.LocalDelete; - documentId: DocumentId | Promise; // if it's a promise, the promise is fulfilled once the document's create event is processed - } + type: SyncEventType.LocalDelete; + documentId: DocumentId | Promise; // if it's a promise, the promise is fulfilled once the document's create event is processed + } | { - type: SyncEventType.RemoteChange; - remoteVersion: DocumentVersionWithoutContent; - }; + type: SyncEventType.RemoteChange; + remoteVersion: DocumentVersionWithoutContent; + }; diff --git a/frontend/sync-client/src/utils/data-structures/locks.test.ts b/frontend/sync-client/src/utils/data-structures/locks.test.ts index fd8894b8..c98bda0b 100644 --- a/frontend/sync-client/src/utils/data-structures/locks.test.ts +++ b/frontend/sync-client/src/utils/data-structures/locks.test.ts @@ -265,7 +265,7 @@ describe("reset", () => { await sleep(1); const secondPromise = locks.withLock(testPath, async () => "second"); - void secondPromise.catch(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function + void secondPromise.catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function locks.reset(); @@ -286,7 +286,7 @@ describe("reset", () => { await sleep(1); const secondPromise = locks.withLock(testPath, async () => "second"); - void secondPromise.catch(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function + void secondPromise.catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function locks.reset(); @@ -312,7 +312,7 @@ describe("reset", () => { [testPath, testPath2], async () => "multi" ); - void multiKeyPromise.catch(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function + void multiKeyPromise.catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function // Wait for the multi-key operation to acquire testPath and start waiting on testPath2 await sleep(10); diff --git a/frontend/sync-client/src/utils/data-structures/locks.ts b/frontend/sync-client/src/utils/data-structures/locks.ts index 4e512869..99c33075 100644 --- a/frontend/sync-client/src/utils/data-structures/locks.ts +++ b/frontend/sync-client/src/utils/data-structures/locks.ts @@ -8,7 +8,7 @@ import type { Logger } from "../../tracing/logger"; * @template T The type of the key used for locking */ /** Waiter entry with callbacks */ -interface WaiterEntry { +interface WaiterEntry { resolve: () => unknown; reject: (err: unknown) => unknown; } @@ -18,9 +18,12 @@ export class Locks { private readonly locked = new Set(); /** Queue of waiters for each key */ - private readonly waiters = new Map[]>(); + private readonly waiters = new Map(); - public constructor(private readonly name: string, private readonly logger?: Logger) { } + public constructor( + private readonly name: string, + private readonly logger?: Logger + ) {} /** * Executes a function while holding exclusive locks on one or more keys. @@ -134,7 +137,7 @@ export class Locks { waiting.push({ resolve, - reject, + reject }); }); } diff --git a/frontend/sync-client/src/utils/data-structures/min-covered.ts b/frontend/sync-client/src/utils/data-structures/min-covered.ts index 720e20a3..f92ef26c 100644 --- a/frontend/sync-client/src/utils/data-structures/min-covered.ts +++ b/frontend/sync-client/src/utils/data-structures/min-covered.ts @@ -16,7 +16,7 @@ export class MinCovered { private seenValues: number[] = []; - public constructor(private minValue: number) { } + public constructor(private minValue: number) {} public get min(): number { return this.minValue; diff --git a/frontend/sync-client/src/utils/debugging/log-to-console.ts b/frontend/sync-client/src/utils/debugging/log-to-console.ts index f38335fe..def71400 100644 --- a/frontend/sync-client/src/utils/debugging/log-to-console.ts +++ b/frontend/sync-client/src/utils/debugging/log-to-console.ts @@ -16,12 +16,12 @@ export function logToConsole( ): void { logger.onLogEmitted.add((logLine: LogLine) => { const timestamp = logLine.timestamp.toISOString(); - const {message} = logLine; + const { message } = logLine; let color = ""; let reset = ""; if (useColors) { - reset = COLORS.reset; + ({ reset } = COLORS); switch (logLine.level) { case LogLevel.ERROR: color = COLORS.red; diff --git a/frontend/sync-client/src/utils/find-matching-file.ts b/frontend/sync-client/src/utils/find-matching-file.ts index 1b8df384..f5d65b39 100644 --- a/frontend/sync-client/src/utils/find-matching-file.ts +++ b/frontend/sync-client/src/utils/find-matching-file.ts @@ -1,4 +1,8 @@ -import type { DocumentRecord, DocumentWithPath, RelativePath } from "../sync-operations/types"; +import type { + DocumentRecord, + DocumentWithPath, + RelativePath +} from "../sync-operations/types"; import { EMPTY_HASH } from "./hash"; // TODO: make this smarter so that offline files can be renamed & edited at the same time @@ -6,7 +10,7 @@ export async function findMatchingFile( contentHash: string, candidates: { path: RelativePath; record: DocumentRecord }[] ): Promise { - if (contentHash === await EMPTY_HASH) { + if (contentHash === (await EMPTY_HASH)) { return undefined; } diff --git a/frontend/sync-client/src/utils/hash.ts b/frontend/sync-client/src/utils/hash.ts index 933929c5..dbda085b 100644 --- a/frontend/sync-client/src/utils/hash.ts +++ b/frontend/sync-client/src/utils/hash.ts @@ -1,8 +1,8 @@ export async function hash(content: Uint8Array): Promise { - const digest = await crypto.subtle.digest( - "SHA-256", - content as Uint8Array - ); + // Copy into a fresh ArrayBuffer-backed Uint8Array so the buffer type + // matches `BufferSource`/`Uint8Array` expected by digest. + const owned = new Uint8Array(content); + const digest = await crypto.subtle.digest("SHA-256", owned); const bytes = new Uint8Array(digest); return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } diff --git a/frontend/sync-client/src/utils/rate-limit.ts b/frontend/sync-client/src/utils/rate-limit.ts index 54373f50..2721de16 100644 --- a/frontend/sync-client/src/utils/rate-limit.ts +++ b/frontend/sync-client/src/utils/rate-limit.ts @@ -44,14 +44,16 @@ export function rateLimit< newArgs = undefined; } - const { promise, resolve } = Promise.withResolvers(); + const { promise, resolve } = Promise.withResolvers(); running = promise; sleep( typeof minIntervalMs === "function" ? minIntervalMs() : minIntervalMs ) - .then(resolve) + .then(() => { + resolve(undefined); + }) .catch(() => { // sleep cannot fail }); diff --git a/frontend/test-client/src/agent/mock-agent.ts b/frontend/test-client/src/agent/mock-agent.ts index 1422ac23..00acc600 100644 --- a/frontend/test-client/src/agent/mock-agent.ts +++ b/frontend/test-client/src/agent/mock-agent.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { choose } from "../utils/choose"; import { v4 as uuidv4 } from "uuid"; import { assert } from "../utils/assert"; @@ -109,7 +110,6 @@ export class MockAgent extends MockClient { ); } - public async act(): Promise { const options: (() => Promise)[] = [ this.createFileAction.bind(this), @@ -125,7 +125,6 @@ export class MockAgent extends MockClient { options.push(this.enableSyncAction.bind(this)); } - options.push( this.renameFileAction.bind(this), this.updateFileAction.bind(this), @@ -136,7 +135,6 @@ export class MockAgent extends MockClient { options.push(this.deleteFileAction.bind(this)); } - if (Math.random() < 0.015 && this.doResets) { // we can't just queue this up as once it's destroyed, no more method calls can go to SyncClient await this.resetClient(); @@ -164,7 +162,7 @@ export class MockAgent extends MockClient { // pending operations. if ( error instanceof Error && - error.message?.includes("SyncClient destroyed") + error.message.includes("SyncClient destroyed") ) { this.client.logger.info( `Action interrupted by destroy: ${error}` @@ -262,17 +260,18 @@ export class MockAgent extends MockClient { "Local files: " + Array.from(this.files.keys()).join(", ") ); otherAgent.client.logger.info( - "Other agent's data: " + JSON.stringify(otherAgent.data, null, 2) + "Other agent's data: " + + JSON.stringify(otherAgent.data, null, 2) ); otherAgent.client.logger.info( - "Other agent's files: " + Array.from(otherAgent.files.keys()).join(", ") + "Other agent's files: " + + Array.from(otherAgent.files.keys()).join(", ") ); throw e; } } - public assertAllContentIsPresentOnce(): void { if (this.useSlowFileEvents) { this.client.logger.info( @@ -349,7 +348,6 @@ export class MockAgent extends MockClient { } } - private async resetClient(): Promise { this.client.logger.info(`Resetting client ${this.name}`); await this.client.destroy(); @@ -372,8 +370,7 @@ export class MockAgent extends MockClient { `Decided to create file ${file} with content ${content}` ); - - return this.write(file, new TextEncoder().encode(` ${content} `),); + return this.write(file, new TextEncoder().encode(` ${content} `)); } // Binary file creation — exercises the putBinary server path (not in mergeable_file_extensions) @@ -393,7 +390,7 @@ export class MockAgent extends MockClient { `Decided to create binary file ${file}: ${uuid}` ); - return this.write(file, bytes,); + return this.write(file, bytes); } private async disableSyncAction(): Promise { @@ -433,9 +430,8 @@ export class MockAgent extends MockClient { // assertion to fail when the sync engine replaces binary content // at a mergeable path). const ext = file.substring(file.lastIndexOf(".")); - const newName = ext === ".bin" - ? this.getBinaryFileName() - : this.getFileName(); + const newName = + ext === ".bin" ? this.getBinaryFileName() : this.getFileName(); if ( (!this.lastSyncEnabledState && @@ -479,14 +475,10 @@ export class MockAgent extends MockClient { `Decided to update file ${file} with ${content}` ); this.doNotTouchWhileOffline.push(file); - await this.atomicUpdateText( - file, - (old) => ({ - text: old.text + ` ${content} `, - cursors: [] - }) - ); - + await this.atomicUpdateText(file, (old) => ({ + text: old.text + ` ${content} `, + cursors: [] + })); } private async updateBinaryFileAction(): Promise { @@ -506,12 +498,10 @@ export class MockAgent extends MockClient { return; } - const { uuid, bytes } = this.getBinaryContent(); + const { uuid: _uuid, bytes } = this.getBinaryContent(); // Remove the old UUID since binary updates are last-write-wins this.removeBinaryUuid(file); - this.client.logger.info( - `Decided to update binary file ${file}` - ); + this.client.logger.info(`Decided to update binary file ${file}`); this.doNotTouchWhileOffline.push(file); await this.write(file, bytes); } @@ -531,7 +521,6 @@ export class MockAgent extends MockClient { `Deleting file: ${file} with:\n content '${new TextDecoder().decode(this.files.get(file))}'` ); await this.delete(file); - } private getContent(): string { @@ -546,8 +535,7 @@ export class MockAgent extends MockClient { const content = new TextDecoder().decode(existing); if (!content.startsWith("BINARY:")) return; const uuid = content.slice("BINARY:".length); - const idx = this.writtenBinaryContents.indexOf(uuid); - if (idx !== -1) this.writtenBinaryContents.splice(idx, 1); + utils.removeFromArray(this.writtenBinaryContents, uuid); } private getBinaryContent(): { uuid: string; bytes: Uint8Array } { diff --git a/frontend/test-client/src/agent/mock-client.ts b/frontend/test-client/src/agent/mock-client.ts index 5d816aa4..84da4167 100644 --- a/frontend/test-client/src/agent/mock-client.ts +++ b/frontend/test-client/src/agent/mock-client.ts @@ -40,7 +40,6 @@ export class MockClient extends debugging.InMemoryFileSystem { await this.client.start(); } - public override async write( path: RelativePath, content: Uint8Array @@ -50,14 +49,14 @@ export class MockClient extends debugging.InMemoryFileSystem { this.files.set(path, content); if (isNew) { - this.executeFileOperation(async () => { this.client.syncLocallyCreatedFile(path); } - ); + this.executeFileOperation(async () => { + this.client.syncLocallyCreatedFile(path); + }); } else { - this.executeFileOperation( - async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); }, - ); + this.executeFileOperation(async () => { + this.client.syncLocallyUpdatedFile({ relativePath: path }); + }); } - } public override async atomicUpdateText( @@ -73,20 +72,18 @@ export class MockClient extends debugging.InMemoryFileSystem { const newContentUint8Array = new TextEncoder().encode(newContent); this.files.set(path, newContentUint8Array); - this.executeFileOperation( - async () => { this.client.syncLocallyUpdatedFile({ relativePath: path }); }, - ); + this.executeFileOperation(async () => { + this.client.syncLocallyUpdatedFile({ relativePath: path }); + }); return newContent; } - - public override async delete(path: RelativePath): Promise { this.files.delete(path); - this.executeFileOperation( - async () => { this.client.syncLocallyDeletedFile(path); }, - ); + this.executeFileOperation(async () => { + this.client.syncLocallyDeletedFile(path); + }); } public override async rename( @@ -101,17 +98,15 @@ export class MockClient extends debugging.InMemoryFileSystem { if (oldPath !== newPath) { this.files.delete(oldPath); } - this.executeFileOperation( - async () => { this.client.syncLocallyUpdatedFile({ + this.executeFileOperation(async () => { + this.client.syncLocallyUpdatedFile({ oldPath, relativePath: newPath - }); }, - ); + }); + }); } - protected executeFileOperation( - callback: () => unknown, - ): void { + protected executeFileOperation(callback: () => unknown): void { if (this.useSlowFileEvents) { // we aren't the best client and it takes some time to notice changes setTimeout(callback, Math.random() * 100); diff --git a/frontend/test-client/src/cli.ts b/frontend/test-client/src/cli.ts index 0fcd975b..ece94cc3 100644 --- a/frontend/test-client/src/cli.ts +++ b/frontend/test-client/src/cli.ts @@ -119,9 +119,6 @@ async function runTest({ await sleep(2000); } - - - for (const client of clients) { try { logger.info(`Destroying ${client.name}`); @@ -254,11 +251,7 @@ process.on("uncaughtException", (error) => { }); process.on("unhandledRejection", (error, _promise) => { - if ( - error instanceof Error && - ( - error.name === "SyncResetError") - ) { + if (error instanceof Error && error.name === "SyncResetError") { return; } diff --git a/frontend/test-client/src/utils/test-error-tracker.ts b/frontend/test-client/src/utils/test-error-tracker.ts index cf40a76c..4620b1e3 100644 --- a/frontend/test-client/src/utils/test-error-tracker.ts +++ b/frontend/test-client/src/utils/test-error-tracker.ts @@ -12,9 +12,7 @@ export class TestErrorTracker { public checkAndThrow(): void { if (this.firstError !== null) { const { agentName, message } = this.firstError; - throw new Error( - `ERROR-level log from ${agentName}: ${message}` - ); + throw new Error(`ERROR-level log from ${agentName}: ${message}`); } } diff --git a/package-lock.json b/package-lock.json index 9e0474fd..a669e690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "vault-link", - "lockfileVersion": 3, - "requires": true, - "packages": {} + "name": "vault-link", + "lockfileVersion": 3, + "requires": true, + "packages": {} } diff --git a/sync-server/config-e2e.yml b/sync-server/config-e2e.yml index d0f76446..242f69ef 100644 --- a/sync-server/config-e2e.yml +++ b/sync-server/config-e2e.yml @@ -10,24 +10,24 @@ server: broadcast_channel_capacity: 1024 response_timeout: 30m mergeable_file_extensions: - - md - - txt + - md + - txt users: user_configs: - - name: admin - token: test-token-change-me - vault_access: - type: allow_access_to_all - - name: other-admin - token: test-token-change-me2 - vault_access: - type: allow_access_to_all - - name: test - token: other-test-token - vault_access: - type: allow_list - allowed: - - default + - name: admin + token: test-token-change-me + vault_access: + type: allow_access_to_all + - name: other-admin + token: test-token-change-me2 + vault_access: + type: allow_access_to_all + - name: test + token: other-test-token + vault_access: + type: allow_list + allowed: + - default logging: log_directory: logs log_rotation: 7days