Improve settings (#168)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e75298c4f1
commit
c08feba0ad
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
|
||||
|
||||
/**
|
||||
* 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,37 +194,27 @@ 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();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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