Refactor & lint

This commit is contained in:
Andras Schmelczer 2025-12-07 15:46:00 +00:00
parent e47d8a8179
commit 6608804d34
16 changed files with 126 additions and 133 deletions

View file

@ -136,10 +136,7 @@ export default class VaultLinkPlugin extends Plugin {
...(IS_DEBUG_BUILD ...(IS_DEBUG_BUILD
? { ? {
fetch: debugging.slowFetchFactory(1), fetch: debugging.slowFetchFactory(1),
webSocket: debugging.slowWebSocketFactory( webSocket: debugging.slowWebSocketFactory(1, new Logger())
1,
new Logger()
)
} }
: {}) : {})
}); });
@ -174,7 +171,7 @@ export default class VaultLinkPlugin extends Plugin {
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]); this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
client.addRemoteCursorsUpdateListener((cursors) => { client.onRemoteCursorsUpdated.add((cursors) => {
RemoteCursorsPluginValue.setCursors(cursors, this.app); RemoteCursorsPluginValue.setCursors(cursors, this.app);
renderCursorsInFileExplorer(cursors, this.app); renderCursorsInFileExplorer(cursors, this.app);
}); });

View file

@ -41,30 +41,28 @@ export class SyncSettingsTab extends PluginSettingTab {
this.editedToken = this.syncClient.getSettings().token; this.editedToken = this.syncClient.getSettings().token;
this.editedVaultName = this.syncClient.getSettings().vaultName; this.editedVaultName = this.syncClient.getSettings().vaultName;
this.syncClient.onSettingsChanged.add( this.syncClient.onSettingsChanged.add((newSettings, oldSettings) => {
(newSettings, oldSettings) => { let hasChanged = false;
let hasChanged = false;
if (newSettings.remoteUri !== oldSettings.remoteUri) { if (newSettings.remoteUri !== oldSettings.remoteUri) {
this.editedServerUri = newSettings.remoteUri; this.editedServerUri = newSettings.remoteUri;
hasChanged = true; hasChanged = true;
}
if (newSettings.token !== oldSettings.token) {
this.editedToken = newSettings.token;
hasChanged = true;
}
if (newSettings.vaultName !== oldSettings.vaultName) {
this.editedVaultName = newSettings.vaultName;
hasChanged = true;
}
if (hasChanged) {
this.display();
}
} }
);
if (newSettings.token !== oldSettings.token) {
this.editedToken = newSettings.token;
hasChanged = true;
}
if (newSettings.vaultName !== oldSettings.vaultName) {
this.editedVaultName = newSettings.vaultName;
hasChanged = true;
}
if (hasChanged) {
this.display();
}
});
} }
private get isApplyingChanges(): boolean { private get isApplyingChanges(): boolean {

View file

@ -114,7 +114,7 @@ export class Database {
i === 0 i === 0
? false ? false
: records[i - 1].parallelVersion === : records[i - 1].parallelVersion ===
current.parallelVersion current.parallelVersion
) )
) { ) {
throw new Error( throw new Error(

View file

@ -33,13 +33,13 @@ export const DEFAULT_SETTINGS: SyncSettings = {
}; };
export class Settings { export class Settings {
private settings: SyncSettings;
private readonly lock: Lock = new Lock();
public readonly onSettingsChanged = new EventListeners< public readonly onSettingsChanged = new EventListeners<
(newSettings: SyncSettings, oldSettings: SyncSettings) => unknown (newSettings: SyncSettings, oldSettings: SyncSettings) => unknown
>(); >();
private settings: SyncSettings;
private readonly lock: Lock = new Lock();
public constructor( public constructor(
private readonly logger: Logger, private readonly logger: Logger,
initialState: Partial<SyncSettings> | undefined, initialState: Partial<SyncSettings> | undefined,

View file

@ -26,7 +26,7 @@ import { FixedSizeDocumentCache } from "./utils/data-structures/fix-sized-cache"
import { setUpTelemetry } from "./utils/set-up-telemetry"; import { setUpTelemetry } from "./utils/set-up-telemetry";
import { DIFF_CACHE_SIZE_MB } from "./consts"; import { DIFF_CACHE_SIZE_MB } from "./consts";
import { ServerConfig } from "./services/server-config"; import { ServerConfig } from "./services/server-config";
import { EventListeners } from "./utils/data-structures/event-listeners"; import type { EventListeners } from "./utils/data-structures/event-listeners";
export class SyncClient { export class SyncClient {
private hasStartedOfflineSync = false; private hasStartedOfflineSync = false;
@ -54,7 +54,7 @@ export class SyncClient {
database: Partial<StoredDatabase>; database: Partial<StoredDatabase>;
}> }>
> >
) { } ) {}
public get documentCount(): number { public get documentCount(): number {
return this.database.length; return this.database.length;
@ -63,6 +63,42 @@ export class SyncClient {
public get isWebSocketConnected(): boolean { public get isWebSocketConnected(): boolean {
return this.webSocketManager.isWebSocketConnected; return this.webSocketManager.isWebSocketConnected;
} }
public get onSyncHistoryUpdated(): EventListeners<
(stats: HistoryStats) => unknown
> {
this.checkIfDestroyed("onSyncHistoryUpdated getter");
return this.history.onHistoryUpdated;
}
public get onSettingsChanged(): EventListeners<
(newSettings: SyncSettings, oldSettings: SyncSettings) => unknown
> {
this.checkIfDestroyed("onSettingsChanged getter");
return this.settings.onSettingsChanged;
}
public get onRemainingOperationsCountChanged(): EventListeners<
(remainingOperationsCount: number) => unknown
> {
this.checkIfDestroyed("onRemainingOperationsCountChanged getter");
return this.syncer.onRemainingOperationsCountChanged;
}
public get onWebSocketStatusChanged(): EventListeners<
(isConnected: boolean) => unknown
> {
this.checkIfDestroyed("onWebSocketStatusChanged getter");
return this.webSocketManager.onWebSocketStatusChanged;
}
public get onRemoteCursorsUpdated(): EventListeners<
(cursors: MaybeOutdatedClientCursors[]) => unknown
> {
this.checkIfDestroyed("onRemoteCursorsUpdated getter");
return this.cursorTracker.onRemoteCursorsUpdated;
}
public static async create({ public static async create({
fs, fs,
persistence, persistence,
@ -228,9 +264,7 @@ export class SyncClient {
} }
}); });
this.settings.onSettingsChanged.add( this.settings.onSettingsChanged.add(this.onSettingsChange.bind(this));
this.onSettingsChange.bind(this)
);
if (this.settings.getSettings().isSyncEnabled) { if (this.settings.getSettings().isSyncEnabled) {
this.logger.info("Starting SyncClient"); this.logger.info("Starting SyncClient");
@ -318,37 +352,6 @@ export class SyncClient {
await this.settings.setSettings(value); await this.settings.setSettings(value);
} }
public get onSyncHistoryUpdated(): EventListeners<
(stats: HistoryStats) => unknown
> {
this.checkIfDestroyed("onSyncHistoryUpdated getter");
return this.history.onHistoryUpdated;
}
public get onSettingsChanged(): EventListeners<
(newSettings: SyncSettings, oldSettings: SyncSettings) => unknown
> {
this.checkIfDestroyed("onSettingsChanged getter");
return this.settings.onSettingsChanged;
}
public get onRemainingOperationsCountChanged(): EventListeners<
(remainingOperationsCount: number) => unknown
> {
this.checkIfDestroyed("onRemainingOperationsCountChanged getter");
return this.syncer.onRemainingOperationsCountChanged;
}
public get onWebSocketStatusChanged(): EventListeners<
(isConnected: boolean) => unknown
> {
this.checkIfDestroyed("onWebSocketStatusChanged getter");
return this.webSocketManager.onWebSocketStatusChanged;
}
public async syncLocallyCreatedFile( public async syncLocallyCreatedFile(
relativePath: RelativePath relativePath: RelativePath
): Promise<void> { ): Promise<void> {
@ -414,14 +417,6 @@ export class SyncClient {
await this.cursorTracker.sendLocalCursorsToServer(documentToCursors); await this.cursorTracker.sendLocalCursorsToServer(documentToCursors);
} }
public get onRemoteCursorsUpdated(): EventListeners<
(cursors: MaybeOutdatedClientCursors[]) => unknown
> {
this.checkIfDestroyed("onRemoteCursorsUpdated getter");
return this.cursorTracker.onRemoteCursorsUpdated;
}
public async waitUntilFinished(): Promise<void> { public async waitUntilFinished(): Promise<void> {
this.checkIfDestroyed("waitUntilIdle"); this.checkIfDestroyed("waitUntilIdle");
await this.syncer.waitUntilFinished(); await this.syncer.waitUntilFinished();

View file

@ -16,14 +16,14 @@ import { EventListeners } from "../utils/data-structures/event-listeners";
// known remote cursor positions, and for each document, tries to return the latest cursor positions that are // known remote cursor positions, and for each document, tries to return the latest cursor positions that are
// not from the future. // not from the future.
export class CursorTracker { export class CursorTracker {
private readonly updateLock = new Lock();
// The returned position may be accurate, if it matches the document version, or outdated, in which case // The returned position may be accurate, if it matches the document version, or outdated, in which case
// the client has to heuristically guess it's current position based on the local edits. // the client has to heuristically guess it's current position based on the local edits.
public readonly onRemoteCursorsUpdated = new EventListeners< public readonly onRemoteCursorsUpdated = new EventListeners<
(cursors: MaybeOutdatedClientCursors[]) => unknown (cursors: MaybeOutdatedClientCursors[]) => unknown
>(); >();
private readonly updateLock = new Lock();
private knownRemoteCursors: (ClientCursors & { private knownRemoteCursors: (ClientCursors & {
upToDateness: DocumentUpToDateness; upToDateness: DocumentUpToDateness;
})[] = []; })[] = [];
@ -72,7 +72,6 @@ export class CursorTracker {
} }
); );
this.fileChangeNotifier.onFileChanged.add(async (relativePath) => this.fileChangeNotifier.onFileChanged.add(async (relativePath) =>
this.updateLock.withLock(async () => { this.updateLock.withLock(async () => {
for (const clientCursor of this.knownRemoteCursors) { for (const clientCursor of this.knownRemoteCursors) {
@ -156,7 +155,6 @@ export class CursorTracker {
this.webSocketManager.updateLocalCursors({ documentsWithCursors }); this.webSocketManager.updateLocalCursors({ documentsWithCursors });
} }
public reset(): void { public reset(): void {
this.knownRemoteCursors = []; this.knownRemoteCursors = [];
this.lastLocalCursorState = []; this.lastLocalCursorState = [];

View file

@ -24,16 +24,18 @@ import { awaitAll } from "../utils/await-all";
import { EventListeners } from "../utils/data-structures/event-listeners"; import { EventListeners } from "../utils/data-structures/event-listeners";
export class Syncer { export class Syncer {
private readonly remoteDocumentsLock: Locks<DocumentId>;
public readonly onRemainingOperationsCountChanged = new EventListeners< public readonly onRemainingOperationsCountChanged = new EventListeners<
(remainingOperations: number) => unknown (remainingOperations: number) => unknown
>(); >();
private readonly remoteDocumentsLock: Locks<DocumentId>;
// FIFO to limit the number of concurrent sync operations // FIFO to limit the number of concurrent sync operations
private readonly syncQueue: PQueue; private readonly syncQueue: PQueue;
private _isFirstSyncComplete = false; private _isFirstSyncComplete = false;
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined; private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
private previousRemainingOperationsCount = 0;
public constructor( public constructor(
private readonly deviceId: string, private readonly deviceId: string,
@ -58,17 +60,20 @@ export class Syncer {
}); });
this.syncQueue.on("active", () => { this.syncQueue.on("active", () => {
this.onRemainingOperationsCountChanged.trigger(this.syncQueue.size); if (this.previousRemainingOperationsCount !== this.syncQueue.size) {
this.previousRemainingOperationsCount = this.syncQueue.size;
this.onRemainingOperationsCountChanged.trigger(
this.syncQueue.size
);
}
}); });
this.webSocketManager.onWebSocketStatusChanged.add( this.webSocketManager.onWebSocketStatusChanged.add((isConnected) => {
(isConnected) => { if (isConnected) {
if (isConnected) { // The JS WebSocket API doesn't support setting headers, so we have to send the token as a message
// The JS WebSocket API doesn't support setting headers, so we have to send the token as a message this.sendHandshakeMessage();
this.sendHandshakeMessage();
}
} }
); });
this.webSocketManager.onRemoteVaultUpdateReceived.add( this.webSocketManager.onRemoteVaultUpdateReceived.add(
this.syncRemotelyUpdatedFile.bind(this) this.syncRemotelyUpdatedFile.bind(this)
); );
@ -166,7 +171,7 @@ export class Syncer {
// in that case, we mustn't move it again. // in that case, we mustn't move it again.
if ( if (
this.database.getLatestDocumentByRelativePath(relativePath) === this.database.getLatestDocumentByRelativePath(relativePath) ===
undefined || undefined ||
this.database.getLatestDocumentByRelativePath(relativePath) this.database.getLatestDocumentByRelativePath(relativePath)
?.isDeleted === true ?.isDeleted === true
) { ) {

View file

@ -333,7 +333,7 @@ export class UnrestrictedSyncer {
const actualUpdateDetails: SyncUpdateDetails | SyncMovedDetails = const actualUpdateDetails: SyncUpdateDetails | SyncMovedDetails =
oldPath !== undefined || oldPath !== undefined ||
response.relativePath != originalRelativePath response.relativePath != originalRelativePath
? { ? {
type: SyncType.MOVE, type: SyncType.MOVE,
relativePath: response.relativePath, relativePath: response.relativePath,
@ -540,8 +540,9 @@ export class UnrestrictedSyncer {
type: SyncType.SKIPPED, type: SyncType.SKIPPED,
relativePath relativePath
}, },
message: `File size of ${sizeInMB} MB exceeds the maximum file size limit of ${maxFileSizeMB message: `File size of ${sizeInMB} MB exceeds the maximum file size limit of ${
} MB` maxFileSizeMB
} MB`
}; };
} }
} }

View file

@ -20,15 +20,15 @@ export class LogLine {
public constructor( public constructor(
public level: LogLevel, public level: LogLevel,
public message: string public message: string
) { } ) {}
} }
export class Logger { export class Logger {
private readonly messages: LogLine[] = [];
public readonly onLogEmitted = new EventListeners< public readonly onLogEmitted = new EventListeners<
(message: LogLine) => unknown (message: LogLine) => unknown
>(); >();
private readonly messages: LogLine[] = [];
public debug(message: string): void { public debug(message: string): void {
this.pushMessage(message, LogLevel.DEBUG); this.pushMessage(message, LogLevel.DEBUG);

View file

@ -70,18 +70,18 @@ export interface HistoryStats {
} }
export class SyncHistory { export class SyncHistory {
private readonly _entries: HistoryEntry[] = [];
public readonly onHistoryUpdated = new EventListeners< public readonly onHistoryUpdated = new EventListeners<
(status: HistoryStats) => unknown (status: HistoryStats) => unknown
>(); >();
private readonly _entries: HistoryEntry[] = [];
private status: HistoryStats = { private status: HistoryStats = {
success: 0, success: 0,
error: 0 error: 0
}; };
public constructor(private readonly logger: Logger) { } public constructor(private readonly logger: Logger) {}
public get entries(): readonly HistoryEntry[] { public get entries(): readonly HistoryEntry[] {
return this._entries; return this._entries;
@ -114,8 +114,6 @@ export class SyncHistory {
this.updateSuccessCount(historyEntry); this.updateSuccessCount(historyEntry);
} }
public reset(): void { public reset(): void {
this._entries.length = 0; this._entries.length = 0;
this.status = { this.status = {
@ -141,8 +139,8 @@ export class SyncHistory {
candidate !== undefined && candidate !== undefined &&
(this._entries[0] === candidate || (this._entries[0] === candidate ||
candidate.timestamp.getTime() + candidate.timestamp.getTime() +
TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS * 1000 > TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS * 1000 >
entry.timestamp.getTime()) entry.timestamp.getTime())
) { ) {
return candidate; return candidate;
} }

View file

@ -8,8 +8,8 @@ export function createClientId(): string {
typeof navigator !== "undefined" typeof navigator !== "undefined"
? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated ? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated
: typeof process !== "undefined" : typeof process !== "undefined"
? process.platform ? process.platform
: "unknown"; : "unknown";
return `vault-link/${packageVersion} (${uuidv4()}; ${platform})`; return `vault-link/${packageVersion} (${uuidv4()}; ${platform})`;
} }

View file

@ -5,7 +5,8 @@ import { EventListeners } from "./event-listeners";
describe("EventListeners", () => { describe("EventListeners", () => {
it("should add & remove listeners", () => { it("should add & remove listeners", () => {
const listeners = new EventListeners<() => void>(); const listeners = new EventListeners<() => void>();
const listener = () => { }; // eslint-disable-next-line @typescript-eslint/no-empty-function
const listener = (): void => {};
listeners.add(listener); listeners.add(listener);
@ -16,10 +17,10 @@ describe("EventListeners", () => {
assert.strictEqual(listeners.count, 0); assert.strictEqual(listeners.count, 0);
}); });
it("should remove listeners using unsubscribe function", () => { it("should remove listeners using unsubscribe function", () => {
const listeners = new EventListeners<() => void>(); const listeners = new EventListeners<() => void>();
const listener = () => { }; // eslint-disable-next-line @typescript-eslint/no-empty-function
const listener = (): void => {};
const unsubscribe = listeners.add(listener); const unsubscribe = listeners.add(listener);
unsubscribe(); unsubscribe();
@ -29,7 +30,8 @@ describe("EventListeners", () => {
it("should return false when removing non-existent listener", () => { it("should return false when removing non-existent listener", () => {
const listeners = new EventListeners<() => void>(); const listeners = new EventListeners<() => void>();
const listener = () => { }; // eslint-disable-next-line @typescript-eslint/no-empty-function
const listener = (): void => {};
const removed = listeners.remove(listener); const removed = listeners.remove(listener);
@ -38,9 +40,12 @@ describe("EventListeners", () => {
it("should handle multiple listeners", () => { it("should handle multiple listeners", () => {
const listeners = new EventListeners<() => void>(); const listeners = new EventListeners<() => void>();
const listener1 = () => { }; // eslint-disable-next-line @typescript-eslint/no-empty-function
const listener2 = () => { }; const listener1 = (): void => {};
const listener3 = () => { }; // eslint-disable-next-line @typescript-eslint/no-empty-function
const listener2 = (): void => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const listener3 = (): void => {};
listeners.add(listener1); listeners.add(listener1);
listeners.add(listener2); listeners.add(listener2);
@ -82,10 +87,10 @@ describe("EventListeners", () => {
let count1 = 0; let count1 = 0;
let count2 = 0; let count2 = 0;
const listener1 = () => { const listener1 = (): void => {
count1++; count1++;
}; };
const listener2 = () => { const listener2 = (): void => {
count2++; count2++;
}; };
@ -127,12 +132,10 @@ describe("EventListeners", () => {
assert.strictEqual(results.length, 3); assert.strictEqual(results.length, 3);
}); });
it("should not trigger cleared listeners", () => { it("should not trigger cleared listeners", () => {
const listeners = new EventListeners<() => void>(); const listeners = new EventListeners<() => void>();
let called = false; let called = false;
const listener = () => { const listener = (): void => {
called = true; called = true;
}; };

View file

@ -2,11 +2,16 @@ import { removeFromArray } from "../remove-from-array";
import { awaitAll } from "../await-all"; import { awaitAll } from "../await-all";
/** /**
* A utility class for managing event listeners with type-safe add/remove operations. * A utility class for managing event listeners with type-safe add/remove operations.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class EventListeners<TListener extends (...args: any[]) => any> { export class EventListeners<TListener extends (...args: any[]) => any> {
private readonly listeners: TListener[] = []; private readonly listeners: TListener[] = [];
public get count(): number {
return this.listeners.length;
}
/** /**
* Adds a new listener to the collection. * Adds a new listener to the collection.
* *
@ -51,6 +56,7 @@ export class EventListeners<TListener extends (...args: any[]) => any> {
await awaitAll( await awaitAll(
this.listeners this.listeners
.map((listener) => { .map((listener) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return listener(...args); return listener(...args);
}) })
.filter((result): result is Promise<unknown> => { .filter((result): result is Promise<unknown> => {
@ -62,10 +68,4 @@ export class EventListeners<TListener extends (...args: any[]) => any> {
public clear(): void { public clear(): void {
this.listeners.length = 0; this.listeners.length = 0;
} }
public get count(): number {
return this.listeners.length;
}
} }

View file

@ -198,14 +198,14 @@ export class MockAgent extends MockClient {
); );
this.client.logger.info( this.client.logger.info(
"Local files: " + "Local files: " +
Array.from(otherAgent.localFiles.keys()).join(", ") Array.from(otherAgent.localFiles.keys()).join(", ")
); );
otherAgent.client.logger.info( otherAgent.client.logger.info(
"Local data: " + JSON.stringify(otherAgent.data, null, 2) "Local data: " + JSON.stringify(otherAgent.data, null, 2)
); );
otherAgent.client.logger.info( otherAgent.client.logger.info(
"Local files: " + "Local files: " +
Array.from(otherAgent.localFiles.keys()).join(", ") Array.from(otherAgent.localFiles.keys()).join(", ")
); );
throw e; throw e;

View file

@ -34,18 +34,15 @@ fi
cd .. cd ..
# Use git ls-files to only check tracked files, respecting .gitignore
if [[ "$FIX_MODE" == true ]]; then
git ls-files | xargs npx eclint fix
else
git ls-files | xargs npx eclint check
fi
cd frontend cd frontend
npm run build npm run build
npm run test npm run test
npm run lint npm run lint
# Use git ls-files to only check tracked files, respecting .gitignore
# We always run in fix mode and then check with git status
git ls-files | xargs npx eclint fix
if [[ "$FIX_MODE" == false ]] && [[ $(git status --porcelain) ]]; then if [[ "$FIX_MODE" == false ]] && [[ $(git status --porcelain) ]]; then
git status --porcelain git status --porcelain
echo "Failing CI because the working directory is not clean after linting" echo "Failing CI because the working directory is not clean after linting"

View file

@ -11,5 +11,6 @@ cd -
cp -r sync-server/bindings/* frontend/sync-client/src/services/types/ cp -r sync-server/bindings/* frontend/sync-client/src/services/types/
cd frontend cd frontend
npm run lint || npx prettier --write sync-client/src/services/types/*.ts npm run lint
git ls-files | xargs npx eclint fix
cd - cd -