Improve settings #168

Merged
schmelczer merged 14 commits from asch/explore into main 2025-11-19 19:53:10 +00:00
19 changed files with 302 additions and 128 deletions

View file

@ -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"]

View file

@ -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
};
}

View file

@ -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"
copilot-pull-request-reviewer[bot] commented 2025-11-18 22:19:54 +00:00 (Migrated from github.com)

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.

		const healthInterval = setInterval(() => {
			void client.checkConnection().then((status) => {
				writeHealthStatus(healthFile, status);
			});
		}, 30 * 1000); // every 30 seconds
		const clearHealthInterval = () => clearInterval(healthInterval);
		process.on("SIGINT", clearHealthInterval);
		process.on("SIGTERM", clearHealthInterval);
		process.on("exit", clearHealthInterval);
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. ```suggestion const healthInterval = setInterval(() => { void client.checkConnection().then((status) => { writeHealthStatus(healthFile, status); }); }, 30 * 1000); // every 30 seconds const clearHealthInterval = () => clearInterval(healthInterval); process.on("SIGINT", clearHealthInterval); process.on("SIGTERM", clearHealthInterval); process.on("exit", clearHealthInterval); ```
});
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) => {

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
copilot-pull-request-reviewer[bot] commented 2025-11-18 22:19:54 +00:00 (Migrated from github.com)

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.

	const obj = value as Record<string, unknown>;
	return (
		typeof obj.isSuccessful === "boolean" &&
		typeof obj.isWebSocketConnected === "boolean" &&
		typeof obj.serverMessage === "string"
	);
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();

View file

@ -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: [

View file

@ -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"
}

View file

@ -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
? {

View file

@ -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)

View file

@ -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 {

View file

@ -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": {

View file

@ -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"
}
}

View file

@ -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 {

View file

@ -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();

View file

@ -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(

View file

@ -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);
});
});

View file

@ -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;

View 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
View file

@ -0,0 +1,6 @@
{
"name": "vault-link",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -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)?;