Fix lint
This commit is contained in:
parent
4b195b070d
commit
18be9f4dd8
19 changed files with 301 additions and 226 deletions
|
|
@ -226,7 +226,7 @@ async function main(): Promise<void> {
|
|||
);
|
||||
|
||||
fileWatcher.stop();
|
||||
await client.waitAndStop();
|
||||
await client.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue