This commit is contained in:
Andras Schmelczer 2025-11-23 20:27:16 +00:00
parent 4b195b070d
commit 18be9f4dd8
19 changed files with 301 additions and 226 deletions

View file

@ -226,7 +226,7 @@ async function main(): Promise<void> {
);
fileWatcher.stop();
await client.waitAndStop();
await client.destroy();
process.exit(1);
}
}

View file

@ -1,8 +1,6 @@
import type { Stat, Vault, Workspace } from "obsidian";
import { MarkdownView, normalizePath } from "obsidian";
import type {
CursorPosition,
TextWithCursors} from "sync-client";
import type { CursorPosition, TextWithCursors } from "sync-client";
import {
utils,
type FileSystemOperations,

View file

@ -49,7 +49,7 @@ export default class VaultLinkPlugin extends Plugin {
this.registerEditorEvents(client);
this.register(() => client.destroy());
this.register(async () => client.destroy());
await client.start();
});
}
@ -58,8 +58,16 @@ export default class VaultLinkPlugin extends Plugin {
new Notice(
"VaultLink has been enabled, check out the docs for tips on getting started!"
);
this.activateView(LogsView.TYPE);
this.activateView(HistoryView.TYPE);
void this.activateView(HistoryView.TYPE).catch((e: unknown) => {
this.syncClient?.logger.error(
`Failed to open history view on enable: ${e}`
);
});
void this.activateView(LogsView.TYPE).catch((e: unknown) => {
this.syncClient?.logger.error(
`Failed to open logs view on enable: ${e}`
);
});
this.openSettings();
}
@ -169,7 +177,9 @@ export default class VaultLinkPlugin extends Plugin {
client,
this.app.workspace
);
this.register(() => cursorListener.dispose);
this.register(() => {
cursorListener.dispose();
});
this.app.workspace.updateOptions();

View file

@ -25,7 +25,7 @@ export class FileOperations {
): [RelativePath, RelativePath] {
const pathParts = path.split("/");
const fileName = pathParts.pop();
if (!fileName || fileName === "") {
if (fileName == null || fileName === "") {
throw new Error(`Path '${path}' cannot be empty`);
}
@ -166,6 +166,10 @@ export class FileOperations {
await this.deletingEmptyParentDirectoriesOfDeletedFile(oldPath);
}
public reset(): void {
this.fs.reset();
}
private async deletingEmptyParentDirectoriesOfDeletedFile(
path: RelativePath
): Promise<void> {
@ -254,8 +258,4 @@ export class FileOperations {
return newName;
}
public reset(): void {
this.fs.reset();
}
}

View file

@ -105,6 +105,10 @@ export class SafeFileSystemOperations implements FileSystemOperations {
);
}
public reset(): void {
this.locks.reset();
}
/**
* Decorate an operation to ensure that the file exists before running it.
* If the operation fails, it will check if the file still exists and throw
@ -138,8 +142,4 @@ export class SafeFileSystemOperations implements FileSystemOperations {
}
}
}
public reset(): void {
this.locks.reset();
}
}

View file

@ -319,13 +319,6 @@ export class Database {
this.saveInTheBackground();
}
private saveInTheBackground(): void {
this.ensureConsistency();
void this.save().catch((error: unknown) => {
this.logger.error(`Error saving data: ${error}`);
});
}
public async save(): Promise<void> {
return this.saveData({
documents: this.resolvedDocuments.map(
@ -362,4 +355,11 @@ export class Database {
);
}
}
private saveInTheBackground(): void {
this.ensureConsistency();
void this.save().catch((error: unknown) => {
this.logger.error(`Error saving data: ${error}`);
});
}
}

View file

@ -1,3 +1,4 @@
import type { Mock } from "node:test";
import { describe, it, mock, beforeEach, afterEach } from "node:test";
import assert from "node:assert";
import { FetchController } from "./fetch-controller";
@ -6,7 +7,9 @@ import { SyncResetError } from "./sync-reset-error";
import { sleep } from "../utils/sleep";
describe("FetchController", () => {
const createMockFetch = (shouldSleep: boolean) =>
const createMockFetch = (
shouldSleep: boolean
): Mock<() => Promise<Response>> =>
mock.fn(async () => {
if (shouldSleep) {
await sleep(30);

View file

@ -24,16 +24,6 @@ export class FetchController {
createPromise<symbol>();
}
private static getUrlFromInput(input: RequestInfo | URL): string {
if (input instanceof URL) {
return input.href;
}
if (typeof input === "string") {
return input;
}
return input.url;
}
/**
* Whether the fetch implementation can immediately send requests once outside of a reset.
*/
@ -58,6 +48,16 @@ export class FetchController {
}
}
private static getUrlFromInput(input: RequestInfo | URL): string {
if (input instanceof URL) {
return input.href;
}
if (typeof input === "string") {
return input;
}
return input.url;
}
/**
* Starts a reset, causing all ongoing and future fetches to be rejected
* with a SyncResetError until finishReset is called.

View file

@ -82,7 +82,7 @@ export class WebSocketManager {
}
public async stop(): Promise<void> {
const [promise, resolve] = createPromise<void>();
const [promise, resolve] = createPromise();
this.resolveDisconnectingPromise = resolve;
this.isStopped = true;
@ -99,7 +99,7 @@ export class WebSocketManager {
await promise;
}
await awaitAll(this.outstandingPromises).then(() => {});
await awaitAll(this.outstandingPromises);
}
public sendHandshakeMessage(
@ -164,10 +164,25 @@ export class WebSocketManager {
);
};
this.webSocket.onmessage = async (event): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const message = JSON.parse(event.data) as WebSocketServerMessage;
return this.handleWebSocketMessage(message);
this.webSocket.onmessage = (event): void => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const message = JSON.parse(
event.data
) as WebSocketServerMessage;
void this.handleWebSocketMessage(message).catch(
(error: unknown) => {
this.logger.error(
`Error handling WebSocket message: ${String(error)}`
);
}
);
} catch (error) {
this.logger.error(
`Error parsing WebSocket message: ${String(error)}`
);
}
};
this.webSocket.onclose = (event): void => {
@ -194,42 +209,58 @@ export class WebSocketManager {
message: WebSocketServerMessage
): Promise<void> {
if (message.type === "vaultUpdate") {
this.outstandingPromises.push(
...this.remoteVaultUpdateListeners.map(async (listener) => {
const promise = listener(message);
return promise.finally(() => {
if (this.outstandingPromises.includes(promise)) {
this.outstandingPromises.splice(
this.outstandingPromises.indexOf(promise),
1
const promises = this.remoteVaultUpdateListeners.map(
async (listener) => {
const trackedPromise = listener(message)
.catch((error: unknown) => {
this.logger.error(
`Error in vault update listener: ${String(error)}`
);
}
});
})
})
.finally(() => {
const index =
this.outstandingPromises.indexOf(
trackedPromise
);
if (index !== -1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.outstandingPromises.splice(index, 1);
}
});
await trackedPromise;
}
);
this.outstandingPromises.push(...promises);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (message.type === "cursorPositions") {
this.logger.debug(
`Received cursor positions for ${JSON.stringify(message.clients)}`
);
this.outstandingPromises.push(
...this.remoteCursorsUpdateListeners.map(async (listener) => {
const promise = listener(
message.clients.filter(
(client) => client.deviceId !== this.deviceId
)
);
return promise.finally(() => {
if (this.outstandingPromises.includes(promise)) {
this.outstandingPromises.splice(
this.outstandingPromises.indexOf(promise),
1
);
}
});
})
const filteredClients = message.clients.filter(
(client) => client.deviceId !== this.deviceId
);
const promises = this.remoteCursorsUpdateListeners.map(
async (listener) => {
const trackedPromise = listener(filteredClients)
.catch((error: unknown) => {
this.logger.error(
`Error in cursor positions listener: ${String(error)}`
);
})
.finally(() => {
const index =
this.outstandingPromises.indexOf(
trackedPromise
);
if (index !== -1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.outstandingPromises.splice(index, 1);
}
});
await trackedPromise;
}
);
this.outstandingPromises.push(...promises);
} else {
this.logger.warn(
`Received unknown message type: ${JSON.stringify(message)}`

View file

@ -30,7 +30,7 @@ export class SyncClient {
private hasStartedOfflineSync = false;
private hasFinishedOfflineSync = false;
private hasStarted = false;
private readonly hasBeenDestroyed = false;
private hasBeenDestroyed = false;
private unloadTelemetry?: () => void;
private constructor(
@ -54,92 +54,6 @@ export class SyncClient {
>
) {}
public async start(): Promise<void> {
this.checkIfDestroyed();
if (this.hasStarted) {
throw new Error("SyncClient has already been started");
}
this.hasStarted = true;
if (
!this.unloadTelemetry &&
this.settings.getSettings().enableTelemetry
) {
this.unloadTelemetry = setUpTelemetry();
}
this.logger.addOnMessageListener((log): void => {
if (log.level === LogLevel.ERROR && Sentry.isInitialized()) {
Sentry.captureMessage(log.message);
}
});
this.settings.addOnSettingsChangeListener(
this.onSettingsChange.bind(this)
);
if (this.settings.getSettings().isSyncEnabled) {
this.logger.info("Starting SyncClient");
await this.startSyncing();
this.logger.info("SyncClient has successfully started");
}
}
/**
* Reload settings from disk overriding current in-memory settings.
* Missing values will be filled in from DEFAULT_SETTINGS rather than
* retaining current in-memory settings.
*/
public async reloadSettings(): Promise<void> {
this.checkIfDestroyed();
const state = (await this.persistence.load()) ?? {
settings: undefined
};
const settings = {
...DEFAULT_SETTINGS,
...(state.settings ?? {})
};
this.setSettings(settings);
}
private async onSettingsChange(
newSettings: SyncSettings,
oldSettings: SyncSettings
): Promise<void> {
this.checkIfDestroyed();
if (
newSettings.vaultName !== oldSettings.vaultName ||
newSettings.remoteUri !== oldSettings.remoteUri
) {
await this.applyChangedConnectionSettings();
}
if (newSettings.isSyncEnabled !== oldSettings.isSyncEnabled) {
if (newSettings.isSyncEnabled) {
await this.startSyncing();
} else {
await this.pause();
}
}
if (newSettings.diffCacheSizeMB !== oldSettings.diffCacheSizeMB) {
this.contentCache.resize(newSettings.diffCacheSizeMB * 1024 * 1024);
}
if (newSettings.enableTelemetry !== oldSettings.enableTelemetry) {
if (newSettings.enableTelemetry) {
this.unloadTelemetry = setUpTelemetry();
} else {
this.unloadTelemetry?.();
}
}
}
public get documentCount(): number {
this.checkIfDestroyed();
@ -151,7 +65,6 @@ export class SyncClient {
return this.webSocketManager.isWebSocketConnected;
}
public static async create({
fs,
persistence,
@ -292,6 +205,58 @@ export class SyncClient {
return client;
}
public async start(): Promise<void> {
this.checkIfDestroyed();
if (this.hasStarted) {
throw new Error("SyncClient has already been started");
}
this.hasStarted = true;
if (
!this.unloadTelemetry &&
this.settings.getSettings().enableTelemetry
) {
this.unloadTelemetry = setUpTelemetry();
}
this.logger.addOnMessageListener((log): void => {
if (log.level === LogLevel.ERROR && Sentry.isInitialized()) {
Sentry.captureMessage(log.message);
}
});
this.settings.addOnSettingsChangeListener(
this.onSettingsChange.bind(this)
);
if (this.settings.getSettings().isSyncEnabled) {
this.logger.info("Starting SyncClient");
await this.startSyncing();
this.logger.info("SyncClient has successfully started");
}
}
/**
* Reload settings from disk overriding current in-memory settings.
* Missing values will be filled in from DEFAULT_SETTINGS rather than
* retaining current in-memory settings.
*/
public async reloadSettings(): Promise<void> {
this.checkIfDestroyed();
const state = (await this.persistence.load()) ?? {
settings: undefined
};
const settings = {
...DEFAULT_SETTINGS,
...(state.settings ?? {})
};
await this.setSettings(settings);
}
public async checkConnection(): Promise<NetworkConnectionStatus> {
this.checkIfDestroyed();
@ -317,19 +282,6 @@ export class SyncClient {
this.history.addSyncHistoryUpdateListener(listener);
}
private async startSyncing(): Promise<void> {
this.checkIfDestroyed();
if (!this.hasStartedOfflineSync) {
this.hasStartedOfflineSync = true;
await this.syncer.scheduleSyncForOfflineChanges();
}
this.hasFinishedOfflineSync = true;
this.fetchController.finishReset();
this.webSocketManager.start();
}
/**
* Wait for the in-flight operations to finish, reset all tracking,
* and the local database but retain the settings.
@ -367,6 +319,8 @@ export class SyncClient {
this.fetchController.startReset();
await this.pause();
this.hasBeenDestroyed = true;
// clean-up memory early
this.resetInMemoryState();
@ -375,24 +329,9 @@ export class SyncClient {
this.unloadTelemetry?.();
}
private async pause(): Promise<void> {
public getSettings(): SyncSettings {
this.checkIfDestroyed();
this.fetchController.startReset();
await this.webSocketManager.stop();
await this.syncer.waitUntilFinished();
await this.database.save(); // flush all changes to disk
}
private resetInMemoryState(): void {
this.history.reset();
this.contentCache.reset();
this.logger.reset();
this.cursorTracker.reset();
this.syncer.reset();
this.fileOperations.reset();
}
public getSettings(): SyncSettings {
return this.settings.getSettings();
}
@ -400,32 +339,44 @@ export class SyncClient {
key: T,
value: SyncSettings[T]
): Promise<void> {
this.checkIfDestroyed();
await this.settings.setSetting(key, value);
}
public async setSettings(value: Partial<SyncSettings>): Promise<void> {
this.checkIfDestroyed();
await this.settings.setSettings(value);
}
public addOnSettingsChangeListener(
listener: (settings: SyncSettings, oldSettings: SyncSettings) => unknown
): void {
this.checkIfDestroyed();
this.settings.addOnSettingsChangeListener(listener);
}
public addRemainingSyncOperationsListener(
listener: (remainingOperations: number) => unknown
): void {
this.checkIfDestroyed();
this.syncer.addRemainingOperationsListener(listener);
}
public addWebSocketStatusChangeListener(listener: () => unknown): void {
this.checkIfDestroyed();
this.webSocketManager.addWebSocketStatusChangeListener(listener);
}
public async syncLocallyCreatedFile(
relativePath: RelativePath
): Promise<void> {
this.checkIfDestroyed();
this.fileChangeNotifier.notifyOfFileChange(relativePath);
return this.syncer.syncLocallyCreatedFile(relativePath);
}
@ -433,6 +384,8 @@ export class SyncClient {
public async syncLocallyDeletedFile(
relativePath: RelativePath
): Promise<void> {
this.checkIfDestroyed();
this.fileChangeNotifier.notifyOfFileChange(relativePath);
return this.syncer.syncLocallyDeletedFile(relativePath);
}
@ -444,6 +397,8 @@ export class SyncClient {
oldPath?: RelativePath;
relativePath: RelativePath;
}): Promise<void> {
this.checkIfDestroyed();
this.fileChangeNotifier.notifyOfFileChange(relativePath);
return this.syncer.syncLocallyUpdatedFile({
oldPath,
@ -454,6 +409,8 @@ export class SyncClient {
public getDocumentSyncingStatus(
relativePath: RelativePath
): DocumentSyncStatus {
this.checkIfDestroyed();
if (!this.settings.getSettings().isSyncEnabled) {
return DocumentSyncStatus.SYNCING_IS_DISABLED;
}
@ -475,15 +432,82 @@ export class SyncClient {
public async updateLocalCursors(
documentToCursors: Record<RelativePath, CursorSpan[]>
): Promise<void> {
this.checkIfDestroyed();
await this.cursorTracker.sendLocalCursorsToServer(documentToCursors);
}
public addRemoteCursorsUpdateListener(
listener: (cursors: MaybeOutdatedClientCursors[]) => unknown
): void {
this.checkIfDestroyed();
this.cursorTracker.addRemoteCursorsUpdateListener(listener);
}
private async startSyncing(): Promise<void> {
this.checkIfDestroyed();
if (!this.hasStartedOfflineSync) {
this.hasStartedOfflineSync = true;
await this.syncer.scheduleSyncForOfflineChanges();
}
this.hasFinishedOfflineSync = true;
this.fetchController.finishReset();
this.webSocketManager.start();
}
private async pause(): Promise<void> {
this.fetchController.startReset();
await this.webSocketManager.stop();
await this.syncer.waitUntilFinished();
await this.database.save(); // flush all changes to disk
}
private resetInMemoryState(): void {
this.history.reset();
this.contentCache.reset();
this.logger.reset();
this.cursorTracker.reset();
this.syncer.reset();
this.fileOperations.reset();
}
private async onSettingsChange(
newSettings: SyncSettings,
oldSettings: SyncSettings
): Promise<void> {
this.checkIfDestroyed();
if (
newSettings.vaultName !== oldSettings.vaultName ||
newSettings.remoteUri !== oldSettings.remoteUri
) {
await this.applyChangedConnectionSettings();
}
if (newSettings.isSyncEnabled !== oldSettings.isSyncEnabled) {
if (newSettings.isSyncEnabled) {
await this.startSyncing();
} else {
await this.pause();
}
}
if (newSettings.diffCacheSizeMB !== oldSettings.diffCacheSizeMB) {
this.contentCache.resize(newSettings.diffCacheSizeMB * 1024 * 1024);
}
if (newSettings.enableTelemetry !== oldSettings.enableTelemetry) {
if (newSettings.enableTelemetry) {
this.unloadTelemetry = setUpTelemetry();
} else {
this.unloadTelemetry?.();
}
}
}
private checkIfDestroyed(): void {
if (this.hasBeenDestroyed) {
throw new Error(

View file

@ -157,6 +157,13 @@ export class CursorTracker {
});
}
public reset(): void {
this.knownRemoteCursors = [];
this.lastLocalCursorState = [];
this.lastLocalCursorStateWithoutDirtyDocuments = [];
this.updateLock.reset();
}
private getRelevantAndPruneKnownClientCursors(): MaybeOutdatedClientCursors[] {
const result: MaybeOutdatedClientCursors[] = [];
const included = new Set<string>();
@ -250,11 +257,4 @@ export class CursorTracker {
? DocumentUpToDateness.UpToDate
: DocumentUpToDateness.Prior;
}
public reset(): void {
this.knownRemoteCursors = [];
this.lastLocalCursorState = [];
this.lastLocalCursorStateWithoutDirtyDocuments = [];
this.updateLock.reset();
}
}

View file

@ -299,6 +299,13 @@ export class Syncer {
}
}
public reset(): void {
this._isFirstSyncComplete = false;
this.syncQueue.clear();
this.remoteDocumentsLock.reset();
this.runningScheduleSyncForOfflineChanges = undefined;
}
private sendHandshakeMessage(): void {
const message: WebSocketClientMessage = {
type: "handshake",
@ -513,11 +520,4 @@ export class Syncer {
this.database.setHasInitialSyncCompleted(true);
}
public reset(): void {
this._isFirstSyncComplete = false;
this.syncQueue.clear();
this.remoteDocumentsLock.reset();
this.runningScheduleSyncForOfflineChanges = undefined;
}
}

View file

@ -9,6 +9,7 @@ type ResolvedTuple<T extends readonly unknown[]> = {
export const awaitAll = async <T extends readonly unknown[]>(
promises: PromiseTuple<T>
): Promise<ResolvedTuple<T>> => {
// eslint-disable-next-line no-restricted-properties
const result = await Promise.allSettled(promises);
for (const res of result) {
if (res.status === "rejected") {
@ -16,7 +17,9 @@ export const awaitAll = async <T extends readonly unknown[]>(
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result.map(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(res) => (res as PromiseFulfilledResult<unknown>).value
) as ResolvedTuple<T>;
};

View file

@ -3,6 +3,8 @@ import assert from "node:assert";
import { Logger } from "../../tracing/logger";
import type { RelativePath } from "../../persistence/database";
import { Locks } from "./locks";
import { awaitAll } from "../await-all";
import { sleep } from "../sleep";
describe("withLock", () => {
const testPath: RelativePath = "test/document/path";
@ -31,7 +33,7 @@ describe("withLock", () => {
let executionCount = 0;
const result = await locks.withLock(testPath, async () => {
executionCount++;
await new Promise((resolve) => setTimeout(resolve, 10));
await sleep(10);
return "async-success";
});
@ -56,19 +58,19 @@ describe("withLock", () => {
// Start two concurrent operations with keys in different orders
const promise1 = locks.withLock([testPath2, testPath], async () => {
executionOrder.push("operation1-start");
await new Promise((resolve) => setTimeout(resolve, 50));
await sleep(50);
executionOrder.push("operation1-end");
return "result1";
});
const promise2 = locks.withLock([testPath, testPath2], async () => {
executionOrder.push("operation2-start");
await new Promise((resolve) => setTimeout(resolve, 50));
await sleep(50);
executionOrder.push("operation2-end");
return "result2";
});
const [result1, result2] = await Promise.all([promise1, promise2]);
const [result1, result2] = await awaitAll([promise1, promise2]);
assert.strictEqual(result1, "result1");
assert.strictEqual(result2, "result2");
@ -86,19 +88,19 @@ describe("withLock", () => {
const promise1 = locks.withLock(testPath, async () => {
executionOrder.push("operation1-start");
await new Promise((resolve) => setTimeout(resolve, 50));
await sleep(50);
executionOrder.push("operation1-end");
return "result1";
});
const promise2 = locks.withLock(testPath, async () => {
executionOrder.push("operation2-start");
await new Promise((resolve) => setTimeout(resolve, 30));
await sleep(30);
executionOrder.push("operation2-end");
return "result2";
});
const [result1, result2] = await Promise.all([promise1, promise2]);
const [result1, result2] = await awaitAll([promise1, promise2]);
assert.strictEqual(result1, "result1");
assert.strictEqual(result2, "result2");
@ -115,19 +117,20 @@ describe("withLock", () => {
const promise1 = locks.withLock(testPath, async () => {
executionOrder.push("operation1-start");
await new Promise((resolve) => setTimeout(resolve, 50));
await sleep(50);
executionOrder.push("operation1-end");
return "result1";
});
const promise2 = locks.withLock(testPath2, async () => {
executionOrder.push("operation2-start");
await new Promise((resolve) => setTimeout(resolve, 30));
await sleep(30);
executionOrder.push("operation2-end");
return "result2";
});
const [result1, result2] = await Promise.all([promise1, promise2]);
const [result1, result2] = await awaitAll([promise1, promise2]);
assert.strictEqual(result1, "result1");
assert.strictEqual(result2, "result2");
@ -159,7 +162,8 @@ describe("withLock", () => {
await assert.rejects(
locks.withLock(testPath, async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
await sleep(10);
throw error;
}),
{ message: "async test error" }
@ -184,30 +188,30 @@ describe("withLock", () => {
// Start first operation that holds the lock
const firstPromise = locks.withLock(testPath, async () => {
executionOrder.push("first-start");
await new Promise((resolve) => setTimeout(resolve, 100));
await sleep(100);
executionOrder.push("first-end");
return "first";
});
// Small delay to ensure first operation starts
await new Promise((resolve) => setTimeout(resolve, 10));
await sleep(10);
// Queue second and third operations
const secondPromise = locks.withLock(testPath, async () => {
executionOrder.push("second-start");
await new Promise((resolve) => setTimeout(resolve, 30));
await sleep(50);
executionOrder.push("second-end");
return "second";
});
const thirdPromise = locks.withLock(testPath, async () => {
executionOrder.push("third-start");
await new Promise((resolve) => setTimeout(resolve, 20));
await sleep(20);
executionOrder.push("third-end");
return "third";
});
const [first, second, third] = await Promise.all([
const [first, second, third] = await awaitAll([
firstPromise,
secondPromise,
thirdPromise

View file

@ -66,6 +66,11 @@ export class Locks<T> {
}
}
public reset(): void {
this.locked.clear();
this.waiters.clear();
}
/**
* Attempts to acquire a lock immediately without waiting.
* Must call `unlock()` if successful.
@ -131,11 +136,6 @@ export class Locks<T> {
this.locked.delete(key);
}
}
public reset(): void {
this.locked.clear();
this.waiters.clear();
}
}
export class Lock {

View file

@ -6,6 +6,7 @@ export function slowWebSocketFactory(
jitterScaleInSeconds: number,
logger: Logger
): typeof WebSocket {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return class FlakyWebSocket extends WebSocket {
private static readonly RECEIVE_KEY = "websocket-receive";
private static readonly SEND_KEY = "websocket-send";

View file

@ -127,8 +127,9 @@ export class MockAgent extends MockClient {
public async finish(): Promise<void> {
await this.client.setSetting("isSyncEnabled", true);
await Promise.allSettled(this.pendingActions);
await this.client.waitAndStop();
// eslint-disable-next-line no-restricted-properties
await Promise.all(this.pendingActions);
await this.client.destroy();
}
public assertFileSystemsAreConsistent(otherAgent: MockAgent): void {

View file

@ -1,6 +1,4 @@
import type { StoredDatabase ,
TextWithCursors
} from "sync-client";
import type { StoredDatabase, TextWithCursors } from "sync-client";
import { assert } from "../utils/assert";
import {
type RelativePath,

View file

@ -53,10 +53,12 @@ async function runTest({
}
try {
// eslint-disable-next-line no-restricted-properties
await Promise.all(clients.map(async (client) => client.init()));
for (let i = 0; i < iterations; i++) {
console.info(`Iteration ${i + 1}/${iterations}`);
// eslint-disable-next-line no-restricted-properties
await Promise.all(clients.map(async (client) => client.act()));
await sleep(100);
}