Improve settings #168
19 changed files with 302 additions and 128 deletions
|
|
@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT"
|
|||
LABEL org.opencontainers.image.authors="andras@schmelczer.dev"
|
||||
|
||||
COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js
|
||||
COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=1 \
|
||||
CMD node /app/healthcheck.js /tmp/vaultlink-health.json
|
||||
|
||||
WORKDIR /vault
|
||||
|
||||
VOLUME ["/vault"]
|
||||
|
||||
ENTRYPOINT ["node", "/app/cli.js"]
|
||||
ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"]
|
||||
CMD ["--help"]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export interface CliArgs {
|
|||
ignorePatterns?: string[];
|
||||
webSocketRetryIntervalMs?: number;
|
||||
logLevel: LogLevel;
|
||||
health?: string;
|
||||
enableTelemetry?: boolean;
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): CliArgs {
|
||||
|
|
@ -51,6 +53,14 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
|
||||
"INFO"
|
||||
)
|
||||
.option(
|
||||
"--health <path>",
|
||||
"[OPTIONAL] Path to health status file for Docker healthcheck"
|
||||
)
|
||||
.option(
|
||||
"--enable-telemetry",
|
||||
"[OPTIONAL] Enable telemetry (disabled by default)"
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
`
|
||||
|
|
@ -78,6 +88,8 @@ Examples:
|
|||
| number
|
||||
| undefined;
|
||||
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
|
||||
const health = opts.health as string | undefined;
|
||||
const enableTelemetry = opts.enableTelemetry as boolean | undefined;
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
||||
|
||||
if (localPath === undefined) {
|
||||
|
|
@ -117,6 +129,8 @@ Examples:
|
|||
maxFileSizeMB: maxFileSizeMb,
|
||||
ignorePatterns: ignorePattern,
|
||||
webSocketRetryIntervalMs: websocketRetryIntervalMs,
|
||||
logLevel
|
||||
logLevel,
|
||||
health,
|
||||
enableTelemetry
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import * as fsSync from "fs";
|
||||
import type { NetworkConnectionStatus } from "sync-client";
|
||||
import {
|
||||
SyncClient,
|
||||
DEFAULT_SETTINGS,
|
||||
|
|
@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher";
|
|||
import { formatLogLine, colorize, styleText } from "./logger-formatter";
|
||||
import packageJson from "../package.json";
|
||||
|
||||
function writeHealthStatus(
|
||||
filePath: string,
|
||||
connectionStatus: NetworkConnectionStatus
|
||||
): void {
|
||||
try {
|
||||
fsSync.writeFileSync(filePath, JSON.stringify(connectionStatus));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to write health status to ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LOG_LEVEL_ORDER = {
|
||||
[LogLevel.DEBUG]: 0,
|
||||
[LogLevel.INFO]: 1,
|
||||
|
|
@ -78,11 +93,13 @@ async function main(): Promise<void> {
|
|||
syncConcurrency:
|
||||
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
|
||||
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
|
||||
diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB,
|
||||
ignorePatterns,
|
||||
webSocketRetryIntervalMs:
|
||||
args.webSocketRetryIntervalMs ??
|
||||
DEFAULT_SETTINGS.webSocketRetryIntervalMs,
|
||||
isSyncEnabled: true
|
||||
isSyncEnabled: true,
|
||||
enableTelemetry: args.enableTelemetry ?? false
|
||||
};
|
||||
|
||||
const client = await SyncClient.create({
|
||||
|
|
@ -119,6 +136,21 @@ async function main(): Promise<void> {
|
|||
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
|
||||
|
|
||||
});
|
||||
|
||||
if (args.health !== undefined) {
|
||||
const healthFile = args.health;
|
||||
const healthInterval = setInterval(() => {
|
||||
void client.checkConnection().then((status) => {
|
||||
writeHealthStatus(healthFile, status);
|
||||
});
|
||||
}, 30 * 1000); // every 30 seconds
|
||||
const clearHealthInterval = (): void => {
|
||||
clearInterval(healthInterval);
|
||||
};
|
||||
process.on("SIGINT", clearHealthInterval);
|
||||
process.on("SIGTERM", clearHealthInterval);
|
||||
process.on("exit", clearHealthInterval);
|
||||
}
|
||||
|
||||
// Add colored log formatter with level filtering
|
||||
client.logger.addOnMessageListener((logLine) => {
|
||||
// Only show messages at or above the configured log level
|
||||
|
|
@ -132,7 +164,10 @@ async function main(): Promise<void> {
|
|||
const fileWatcher = new FileWatcher(absolutePath, client);
|
||||
|
||||
client.addWebSocketStatusChangeListener(() => {
|
||||
client.logger.info("WebSocket status changed");
|
||||
const isConnected = client.isWebSocketConnected;
|
||||
client.logger.info(
|
||||
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
|
||||
);
|
||||
});
|
||||
|
||||
client.addRemainingSyncOperationsListener((remaining) => {
|
||||
|
|
|
|||
66
frontend/local-client-cli/src/healthcheck.ts
Normal file
66
frontend/local-client-cli/src/healthcheck.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env node
|
||||
|
The type guard always returns true for any non-null object, failing to validate the actual structure of NetworkConnectionStatus. This will pass invalid objects that lack required properties like isSuccessful, isWebSocketConnected, or serverMessage, leading to runtime errors when those properties are accessed on line 44.
The type guard always returns true for any non-null object, failing to validate the actual structure of NetworkConnectionStatus. This will pass invalid objects that lack required properties like isSuccessful, isWebSocketConnected, or serverMessage, leading to runtime errors when those properties are accessed on line 44.
```suggestion
const obj = value as Record<string, unknown>;
return (
typeof obj.isSuccessful === "boolean" &&
typeof obj.isWebSocketConnected === "boolean" &&
typeof obj.serverMessage === "string"
);
```
|
||||
|
||||
/**
|
||||
* Healthcheck script for Docker container
|
||||
* Checks if the sync client is connected to the server
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import type { NetworkConnectionStatus } from "sync-client";
|
||||
|
||||
function isHealthStatus(value: unknown): value is NetworkConnectionStatus {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
"isSuccessful" in value &&
|
||||
typeof value.isSuccessful === "boolean" &&
|
||||
"isWebSocketConnected" in value &&
|
||||
typeof value.isWebSocketConnected === "boolean" &&
|
||||
"serverMessage" in value &&
|
||||
typeof value.serverMessage === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
if (process.argv.length < 3) {
|
||||
console.error("Usage: healthcheck <path-to-health-file>");
|
||||
process.exit(1);
|
||||
}
|
||||
const [, , healthFile] = process.argv;
|
||||
|
||||
try {
|
||||
// Check if health file exists
|
||||
if (!fs.existsSync(healthFile)) {
|
||||
console.error(`Health file does not exist: ${healthFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and parse health status
|
||||
const content = fs.readFileSync(healthFile, "utf-8");
|
||||
const parsed: unknown = JSON.parse(content);
|
||||
|
||||
// Validate the parsed object using type guard
|
||||
if (!isHealthStatus(parsed)) {
|
||||
throw new Error("Invalid health status format");
|
||||
}
|
||||
|
||||
const status = parsed;
|
||||
|
||||
if (!status.isSuccessful || !status.isWebSocketConnected) {
|
||||
console.error("Not connected to server: " + status.serverMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Healthy: Connected to server");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Health check failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -2,7 +2,10 @@ const path = require("path");
|
|||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/cli.ts",
|
||||
entry: {
|
||||
cli: "./src/cli.ts",
|
||||
healthcheck: "./src/healthcheck.ts"
|
||||
},
|
||||
target: "node",
|
||||
mode: "production",
|
||||
optimization: {
|
||||
|
|
@ -21,7 +24,7 @@ module.exports = {
|
|||
},
|
||||
output: {
|
||||
globalObject: "this",
|
||||
filename: "cli.js",
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "dist")
|
||||
},
|
||||
plugins: [
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@
|
|||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@plausible-analytics/tracker": "^0.4.3",
|
||||
"@sentry/browser": "^10.8.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"date-fns": "^4.1.0",
|
||||
|
|
@ -22,7 +20,7 @@
|
|||
"fs-extra": "^11.3.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"obsidian": "1.10.2",
|
||||
"reconcile-text": "^0.5.0",
|
||||
"reconcile-text": "^0.7.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.91.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
|
|
@ -33,7 +31,6 @@
|
|||
"tsx": "^4.20.5",
|
||||
"typescript": "5.8.3",
|
||||
"url": "^0.11.4",
|
||||
"virtual-scroller": "^1.13.1",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import { HistoryView } from "./views/history/history-view";
|
|||
import { StatusBar } from "./views/status-bar/status-bar";
|
||||
import { LogsView } from "./views/logs/logs-view";
|
||||
import { StatusDescription } from "./views/status-description/status-description";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { init as plausibleInit } from "@plausible-analytics/tracker";
|
||||
import {
|
||||
SyncClient,
|
||||
rateLimit,
|
||||
|
|
@ -50,45 +48,6 @@ export default class VaultLinkPlugin extends Plugin {
|
|||
".trash/**"
|
||||
);
|
||||
|
||||
plausibleInit({
|
||||
domain: "vault-link",
|
||||
endpoint: "https://stats.schmelczer.dev/status",
|
||||
autoCapturePageviews: true,
|
||||
captureOnLocalhost: true,
|
||||
logging: true
|
||||
});
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1",
|
||||
skipBrowserExtensionCheck: false
|
||||
});
|
||||
|
||||
const onError = (event: ErrorEvent): void => {
|
||||
Sentry.captureException(event.error, {
|
||||
extra: {
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener("error", onError);
|
||||
this.disposables.push(() => {
|
||||
window.removeEventListener("error", onError);
|
||||
});
|
||||
|
||||
const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
|
||||
Sentry.captureException(event.reason);
|
||||
};
|
||||
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
||||
this.disposables.push(() => {
|
||||
window.removeEventListener(
|
||||
"unhandledrejection",
|
||||
onUnhandledRejection
|
||||
);
|
||||
});
|
||||
|
||||
const isDebugBuild = process.env.NODE_ENV === "development";
|
||||
const debugOptions = isDebugBuild
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class RemoteCursorsPluginValue implements PluginValue {
|
|||
isOutdated: boolean;
|
||||
}[] = [];
|
||||
|
||||
private static app: App;
|
||||
private static app?: App;
|
||||
public decorations: DecorationSet = RangeSet.of([]);
|
||||
|
||||
public static setCursors(
|
||||
|
|
@ -88,7 +88,7 @@ export class RemoteCursorsPluginValue implements PluginValue {
|
|||
private static findFileForEditor(
|
||||
editor: EditorView
|
||||
): RelativePath | undefined {
|
||||
return RemoteCursorsPluginValue.app.workspace
|
||||
return RemoteCursorsPluginValue.app?.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof MarkdownView)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
this.renderSettingsHeader(containerEl);
|
||||
this.renderConnectionSettings(containerEl);
|
||||
this.renderSyncSettings(containerEl);
|
||||
this.renderMiscSettings(containerEl);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
|
|
@ -193,38 +194,28 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Apply").onClick(async () => {
|
||||
new Setting(containerEl).addButton((button) =>
|
||||
button
|
||||
.setButtonText("Apply & test connection")
|
||||
.onClick(async () => {
|
||||
if (this.areThereUnsavedChanges()) {
|
||||
await this.syncClient.setSettings({
|
||||
vaultName: this.editedVaultName,
|
||||
remoteUri: this.editedServerUri,
|
||||
token: this.editedToken
|
||||
});
|
||||
new Notice("Checking connection to the server...");
|
||||
new Notice(
|
||||
"The changes have been applied successfully!"
|
||||
(
|
||||
await this.syncClient.checkConnection()
|
||||
).serverMessage
|
||||
);
|
||||
await this.statusDescription.updateConnectionState();
|
||||
} else {
|
||||
new Notice("No changes to apply");
|
||||
}
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Test connection").onClick(async () => {
|
||||
if (this.areThereUnsavedChanges()) {
|
||||
new Notice(
|
||||
"There are unsaved changes, testing with the currently saved settings"
|
||||
);
|
||||
}
|
||||
|
||||
new Notice(
|
||||
(await this.syncClient.checkConnection()).serverMessage
|
||||
);
|
||||
await this.statusDescription.updateConnectionState();
|
||||
})
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
private areThereUnsavedChanges(): boolean {
|
||||
|
|
@ -339,6 +330,26 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
);
|
||||
}
|
||||
|
||||
private renderMiscSettings(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("h3", { text: "Other" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Enable telemetry")
|
||||
.setDesc(
|
||||
"Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties."
|
||||
)
|
||||
.setTooltip(
|
||||
"Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.syncClient.getSettings().enableTelemetry)
|
||||
.onChange(async (value) =>
|
||||
this.syncClient.setSetting("enableTelemetry", value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private setStatusDescriptionSubscription(
|
||||
newSubscription?: () => unknown
|
||||
): void {
|
||||
|
|
|
|||
39
frontend/package-lock.json
generated
39
frontend/package-lock.json
generated
|
|
@ -871,13 +871,6 @@
|
|||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@plausible-analytics/tracker": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.3.tgz",
|
||||
"integrity": "sha512-RKTgH5xu7Pa77VS4OEnS4woPhDxRgWLJlt9f6JhwgBC9ilknCfJIVEN2A1D8OR7hzgxMQF/hPyls9iN9ReAm3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.8.0.tgz",
|
||||
|
|
@ -3488,10 +3481,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/reconcile-text": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.5.0.tgz",
|
||||
"integrity": "sha512-zki3lqw9Oxdhm9ZvDN17VyYoL1Isc8BEL07ILVDE2yGfNEI7thrkczoNCUr+hkFU2rzZtfxECTG0b7p61AJ6wg==",
|
||||
"dev": true,
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz",
|
||||
"integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regex-parser": {
|
||||
|
|
@ -3499,11 +3491,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/request-animation-frame-timeout": {
|
||||
"version": "2.0.4",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"dev": true,
|
||||
|
|
@ -4329,14 +4316,6 @@
|
|||
"resolved": "obsidian-plugin",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/virtual-scroller": {
|
||||
"version": "1.13.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"request-animation-frame-timeout": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
|
|
@ -4661,8 +4640,6 @@
|
|||
"version": "0.10.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@plausible-analytics/tracker": "^0.4.3",
|
||||
"@sentry/browser": "^10.8.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"date-fns": "^4.1.0",
|
||||
|
|
@ -4670,7 +4647,7 @@
|
|||
"fs-extra": "^11.3.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"obsidian": "1.10.2",
|
||||
"reconcile-text": "^0.5.0",
|
||||
"reconcile-text": "^0.7.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.91.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
|
|
@ -4681,7 +4658,6 @@
|
|||
"tsx": "^4.20.5",
|
||||
"typescript": "5.8.3",
|
||||
"url": "^0.11.4",
|
||||
"virtual-scroller": "^1.13.1",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
|
|
@ -4696,6 +4672,7 @@
|
|||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/browser": "^10.8.0",
|
||||
"@types/node": "^24.8.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "2.8.1",
|
||||
|
|
@ -4729,12 +4706,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"sync-client/node_modules/reconcile-text": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz",
|
||||
"integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"test-client": {
|
||||
"version": "0.10.0",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-merge": "^6.0.1",
|
||||
"@sentry/browser": "^10.8.0",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export interface SyncSettings {
|
|||
maxFileSizeMB: number;
|
||||
ignorePatterns: string[];
|
||||
webSocketRetryIntervalMs: number;
|
||||
diffCacheSizeMB: number;
|
||||
enableTelemetry: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: SyncSettings = {
|
||||
|
|
@ -19,7 +21,9 @@ export const DEFAULT_SETTINGS: SyncSettings = {
|
|||
isSyncEnabled: false,
|
||||
maxFileSizeMB: 10,
|
||||
ignorePatterns: [],
|
||||
webSocketRetryIntervalMs: 3500
|
||||
webSocketRetryIntervalMs: 3500,
|
||||
diffCacheSizeMB: 4,
|
||||
enableTelemetry: false
|
||||
};
|
||||
|
||||
export class Settings {
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ import type { CursorSpan } from "./services/types/CursorSpan";
|
|||
import type { MaybeOutdatedClientCursors } from "./types/maybe-outdated-client-cursors";
|
||||
import { FileChangeNotifier } from "./sync-operations/file-change-notifier";
|
||||
import { FixedSizeDocumentCache } from "./utils/fix-sized-cache";
|
||||
import { setUpTelemetry } from "./utils/set-up-telemetry";
|
||||
|
||||
export class SyncClient {
|
||||
private static readonly MINIMUM_SAVE_INTERVAL_MS = 1000;
|
||||
private hasStartedOfflineSync = false;
|
||||
private hasFinishedOfflineSync = false;
|
||||
private unloadTelemetry?: () => void;
|
||||
|
||||
private constructor(
|
||||
private readonly history: SyncHistory,
|
||||
|
|
@ -38,8 +40,13 @@ export class SyncClient {
|
|||
private readonly _logger: Logger,
|
||||
private readonly connectionStatus: ConnectionStatus,
|
||||
private readonly cursorTracker: CursorTracker,
|
||||
private readonly fileChangeNotifier: FileChangeNotifier
|
||||
private readonly fileChangeNotifier: FileChangeNotifier,
|
||||
private readonly contentCache: FixedSizeDocumentCache
|
||||
) {
|
||||
if (settings.getSettings().enableTelemetry) {
|
||||
this.unloadTelemetry = setUpTelemetry();
|
||||
}
|
||||
|
||||
this.settings.addOnSettingsChangeListener(
|
||||
async (newSettings, oldSettings) => {
|
||||
if (newSettings.vaultName !== oldSettings.vaultName) {
|
||||
|
|
@ -53,6 +60,24 @@ export class SyncClient {
|
|||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
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?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -65,6 +90,10 @@ export class SyncClient {
|
|||
return this.database.length;
|
||||
}
|
||||
|
||||
public get isWebSocketConnected(): boolean {
|
||||
return this.webSocketManager.isWebSocketConnected;
|
||||
}
|
||||
|
||||
public static async create({
|
||||
fs,
|
||||
persistence,
|
||||
|
|
@ -152,8 +181,7 @@ export class SyncClient {
|
|||
settings,
|
||||
syncService,
|
||||
fileOperations,
|
||||
unrestrictedSyncer,
|
||||
contentCache
|
||||
unrestrictedSyncer
|
||||
);
|
||||
|
||||
const webSocketManager = new WebSocketManager(
|
||||
|
|
@ -182,7 +210,8 @@ export class SyncClient {
|
|||
logger,
|
||||
connectionStatus,
|
||||
cursorTracker,
|
||||
fileChangeNotifier
|
||||
fileChangeNotifier,
|
||||
contentCache
|
||||
);
|
||||
|
||||
logger.info("SyncClient initialised");
|
||||
|
|
@ -235,6 +264,7 @@ export class SyncClient {
|
|||
public async reset(): Promise<void> {
|
||||
this.stop();
|
||||
this.connectionStatus.startReset();
|
||||
this.contentCache.clear();
|
||||
await this.syncer.reset();
|
||||
this.history.reset();
|
||||
this.database.reset();
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ export class Syncer {
|
|||
settings: Settings,
|
||||
private readonly syncService: SyncService,
|
||||
private readonly operations: FileOperations,
|
||||
private readonly internalSyncer: UnrestrictedSyncer,
|
||||
private readonly contentCache: FixedSizeDocumentCache
|
||||
private readonly internalSyncer: UnrestrictedSyncer
|
||||
) {
|
||||
this.syncQueue = new PQueue({
|
||||
concurrency: settings.getSettings().syncConcurrency
|
||||
|
|
@ -252,7 +251,6 @@ export class Syncer {
|
|||
|
||||
public async reset(): Promise<void> {
|
||||
await this.waitUntilFinished();
|
||||
this.contentCache.clear();
|
||||
}
|
||||
|
||||
public async syncRemotelyUpdatedFile(
|
||||
|
|
|
|||
|
|
@ -236,4 +236,40 @@ describe("fixedSizeDocumentCache", () => {
|
|||
assert.equal(cache.get(2), doc2);
|
||||
assert.equal(cache.get(3), doc3);
|
||||
});
|
||||
|
||||
it("resizeToLargerSizeNoEviction", async () => {
|
||||
const cache = new FixedSizeDocumentCache(4);
|
||||
const doc1 = new Uint8Array([1, 2]);
|
||||
const doc2 = new Uint8Array([3, 4]);
|
||||
|
||||
cache.put(1, doc1);
|
||||
cache.put(2, doc2);
|
||||
|
||||
cache.resize(10);
|
||||
|
||||
assert.equal(cache.get(1), doc1);
|
||||
assert.equal(cache.get(2), doc2);
|
||||
});
|
||||
|
||||
it("resizeCausesMultipleEvictions", async () => {
|
||||
const cache = new FixedSizeDocumentCache(10);
|
||||
const doc1 = new Uint8Array([1, 2]);
|
||||
const doc2 = new Uint8Array([3, 4]);
|
||||
const doc3 = new Uint8Array([5, 6]);
|
||||
const doc4 = new Uint8Array([7, 8]);
|
||||
|
||||
cache.put(1, doc1);
|
||||
cache.put(2, doc2);
|
||||
cache.put(3, doc3);
|
||||
cache.put(4, doc4);
|
||||
// Cache has 8 bytes total
|
||||
|
||||
cache.resize(2);
|
||||
|
||||
// Should evict doc1, doc2, doc3 to get down to 2 bytes
|
||||
assert.equal(cache.get(1), undefined);
|
||||
assert.equal(cache.get(2), undefined);
|
||||
assert.equal(cache.get(3), undefined);
|
||||
assert.equal(cache.get(4), doc4);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ class LRUNode {
|
|||
|
||||
// evicting the least recently used documents when the size limit is exceeded.
|
||||
export class FixedSizeDocumentCache {
|
||||
private readonly maxSizeInBytes: number;
|
||||
private currentSizeInBytes: number;
|
||||
private readonly cache: Map<VaultUpdateId, LRUNode>;
|
||||
private head: LRUNode | null; // Least recently used
|
||||
private tail: LRUNode | null; // Most recently used
|
||||
|
||||
public constructor(maxSizeInBytes: number) {
|
||||
this.maxSizeInBytes = maxSizeInBytes;
|
||||
public constructor(private maxSizeInBytes: number) {
|
||||
this.currentSizeInBytes = 0;
|
||||
this.cache = new Map();
|
||||
this.head = null;
|
||||
|
|
@ -56,14 +54,7 @@ export class FixedSizeDocumentCache {
|
|||
this.cache.set(updateId, newNode);
|
||||
this.addToTail(newNode);
|
||||
this.currentSizeInBytes += content.byteLength;
|
||||
|
||||
// Evict least recently used documents if over size limit
|
||||
while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) {
|
||||
const lruNode = this.head;
|
||||
this.removeNode(lruNode);
|
||||
this.cache.delete(lruNode.key);
|
||||
this.currentSizeInBytes -= lruNode.value.byteLength;
|
||||
}
|
||||
this.fitBelowMaxSize();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
|
|
@ -73,6 +64,21 @@ export class FixedSizeDocumentCache {
|
|||
this.currentSizeInBytes = 0;
|
||||
}
|
||||
|
||||
public resize(newMaxSizeInBytes: number): void {
|
||||
this.maxSizeInBytes = newMaxSizeInBytes;
|
||||
this.fitBelowMaxSize();
|
||||
}
|
||||
|
||||
private fitBelowMaxSize(): void {
|
||||
// Evict least recently used documents if over size limit
|
||||
while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) {
|
||||
const lruNode = this.head;
|
||||
this.removeNode(lruNode);
|
||||
this.cache.delete(lruNode.key);
|
||||
this.currentSizeInBytes -= lruNode.value.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
private removeNode(node: LRUNode): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
|
|
|
|||
33
frontend/sync-client/src/utils/set-up-telemetry.ts
Normal file
33
frontend/sync-client/src/utils/set-up-telemetry.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
export const setUpTelemetry = (): (() => void) => {
|
||||
Sentry.init({
|
||||
dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1",
|
||||
skipBrowserExtensionCheck: false
|
||||
});
|
||||
|
||||
const onError = (event: ErrorEvent): void => {
|
||||
Sentry.captureException(event.error, {
|
||||
extra: {
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener("error", onError);
|
||||
|
||||
const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
|
||||
Sentry.captureException(event.reason);
|
||||
};
|
||||
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener("error", onError);
|
||||
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
||||
Sentry.close(5000).catch(() => {
|
||||
// Ignore errors during shutdown
|
||||
});
|
||||
};
|
||||
};
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "vault-link",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -92,9 +92,9 @@ fn set_up_logging(
|
|||
.with_line_number(is_debug_mode)
|
||||
.compact();
|
||||
|
||||
let stdout_layer = tracing_subscriber::fmt::layer()
|
||||
let stderr_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(use_colors)
|
||||
.with_writer(std::io::stdout)
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(format.clone());
|
||||
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
|
|
@ -104,8 +104,8 @@ fn set_up_logging(
|
|||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(stdout_layer)
|
||||
.with(file_layer)
|
||||
.with(stderr_layer)
|
||||
.try_init()
|
||||
.context("Failed to initialise tracing")
|
||||
.map_err(init_error)?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue
The setInterval creates an untracked timer that will continue running indefinitely. Consider storing the timer reference and clearing it during cleanup or process termination to prevent potential resource leaks.