WIP: Quality of life features #180
174 changed files with 21319 additions and 17689 deletions
|
|
@ -11,5 +11,6 @@ indent_style = space
|
|||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
[*.{yml,yaml,md}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
|
|
|||
6
.github/workflows/deploy-docs.yml
vendored
6
.github/workflows/deploy-docs.yml
vendored
|
|
@ -42,14 +42,10 @@ jobs:
|
|||
cd docs
|
||||
npm ci
|
||||
|
||||
- name: Check formatting
|
||||
- name: Check formatting & spelling
|
||||
run: |
|
||||
cd docs
|
||||
npm run format:check
|
||||
|
||||
- name: Check spelling
|
||||
run: |
|
||||
cd docs
|
||||
npm run spell:check
|
||||
|
||||
- name: Build documentation
|
||||
|
|
|
|||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
- cron: '0 * * * *'
|
||||
|
||||
concurrency:
|
||||
group: e2e-tests
|
||||
|
|
|
|||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
.vitepress/.temp/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { defineConfig } from "vitepress"
|
|||
export default defineConfig({
|
||||
title: "VaultLink",
|
||||
description: "Self-hosted real-time synchronisation for Obsidian",
|
||||
base: "/vault-link/",
|
||||
base: "/",
|
||||
themeConfig: {
|
||||
logo: "/logo.svg",
|
||||
nav: [
|
||||
|
|
@ -56,5 +56,5 @@ export default defineConfig({
|
|||
provider: "local"
|
||||
}
|
||||
},
|
||||
head: [["link", { rel: "icon", type: "image/svg+xml", href: "/vault-link/logo.svg" }]]
|
||||
head: [["link", { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }]]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ sequenceDiagram
|
|||
```
|
||||
┌─────────┐
|
||||
│ Client │
|
||||
└────┬────┘
|
||||
└───┬─-───┘
|
||||
│ 1. Detect file change
|
||||
│
|
||||
├─► 2. Read file content
|
||||
|
|
@ -142,7 +142,7 @@ sequenceDiagram
|
|||
▼
|
||||
┌─────────┐
|
||||
│ Server │
|
||||
└────┬────┘
|
||||
└───┬────-┘
|
||||
│ 4. Validate message
|
||||
│
|
||||
├─► 5. Check permissions
|
||||
|
|
@ -163,7 +163,7 @@ sequenceDiagram
|
|||
```
|
||||
┌─────────┐
|
||||
│ Server │
|
||||
└────┬────┘
|
||||
└───┬─-───┘
|
||||
│ 1. File updated by another client
|
||||
│
|
||||
├─► 2. Broadcast notification
|
||||
|
|
@ -176,7 +176,7 @@ sequenceDiagram
|
|||
▼
|
||||
┌─────────┐
|
||||
│ Client │
|
||||
└────┬────┘
|
||||
└───┬─-───┘
|
||||
│ 3. Receive notification
|
||||
│
|
||||
├─► 4. Request file download
|
||||
|
|
@ -189,7 +189,7 @@ sequenceDiagram
|
|||
▼
|
||||
┌─────────┐
|
||||
│ Server │
|
||||
└────┬────┘
|
||||
└───┬─=───┘
|
||||
│ 5. Retrieve from database
|
||||
│
|
||||
└─► 6. Send file content
|
||||
|
|
@ -203,7 +203,7 @@ sequenceDiagram
|
|||
▼
|
||||
┌─────────┐
|
||||
│ Client │
|
||||
└────┬────┘
|
||||
└───-─┬───┘
|
||||
│ 7. Write to filesystem
|
||||
│
|
||||
└─► 8. Update local metadata
|
||||
|
|
|
|||
|
|
@ -55,6 +55,25 @@ export default [
|
|||
message: "Use replaceAll instead of replace to replace all occurrences of a substring."
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='splice'][arguments.length=2][arguments.1.type='Literal'][arguments.1.value=1]",
|
||||
message: "Use `removeFromArray(array, item)` instead of manually using indexOf + splice(index, 1). Import from 'sync-client/src/utils/remove-from-array'."
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='filter'] > ArrowFunctionExpression[body.type='BinaryExpression'][body.operator='!==']",
|
||||
message: "Use `removeFromArray(array, item)` instead of filter(x => x !== item) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='filter'] > ArrowFunctionExpression > BlockStatement > ReturnStatement > BinaryExpression[operator='!==']",
|
||||
message: "Use `removeFromArray(array, item)` instead of filter(x => { return x !== item }) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.property.name='filter'] > FunctionExpression[body.type='BlockStatement'] > BlockStatement > ReturnStatement > BinaryExpression[operator='!==']",
|
||||
message: "Use `removeFromArray(array, item)` instead of filter(function(x) { return x !== item }) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
|
||||
}
|
||||
],
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@
|
|||
"scripts": {
|
||||
"dev": "webpack watch --mode development",
|
||||
"build": "webpack --mode production",
|
||||
"test": "tsx --test src/args.test.ts src/node-filesystem.test.ts"
|
||||
"test": "tsx --test 'src/**/*.test.ts'"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^14.0.2"
|
||||
"commander": "^14.0.2",
|
||||
"watcher": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.8.1",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ async function main(): Promise<void> {
|
|||
const args = parseArgs(process.argv);
|
||||
const absolutePath = path.resolve(args.localPath);
|
||||
|
||||
if (!fsSync.existsSync(absolutePath)) {
|
||||
fsSync.mkdirSync(absolutePath, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (!stats.isDirectory()) {
|
||||
|
|
@ -153,7 +157,7 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
// Add colored log formatter with level filtering
|
||||
client.logger.addOnMessageListener((logLine) => {
|
||||
client.logger.onLogEmitted.add((logLine) => {
|
||||
// Only show messages at or above the configured log level
|
||||
if (LOG_LEVEL_ORDER[logLine.level] >= LOG_LEVEL_ORDER[args.logLevel]) {
|
||||
console.log(formatLogLine(logLine));
|
||||
|
|
@ -164,14 +168,14 @@ async function main(): Promise<void> {
|
|||
|
||||
const fileWatcher = new FileWatcher(absolutePath, client);
|
||||
|
||||
client.addWebSocketStatusChangeListener(() => {
|
||||
client.onWebSocketStatusChanged.add(() => {
|
||||
const isConnected = client.isWebSocketConnected;
|
||||
client.logger.info(
|
||||
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
|
||||
);
|
||||
});
|
||||
|
||||
client.addRemainingSyncOperationsListener((remaining) => {
|
||||
client.onRemainingOperationsCountChanged.add((remaining) => {
|
||||
if (remaining === 0) {
|
||||
client.logger.info("All sync operations completed");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as fs from "fs";
|
||||
import Watcher from "watcher";
|
||||
import * as path from "path";
|
||||
import type { SyncClient, RelativePath } from "sync-client";
|
||||
|
||||
export class FileWatcher {
|
||||
private watcher: fs.FSWatcher | undefined;
|
||||
private watcher: Watcher | undefined;
|
||||
private isRunning = false;
|
||||
|
||||
public constructor(
|
||||
|
|
@ -18,25 +18,31 @@ export class FileWatcher {
|
|||
|
||||
this.isRunning = true;
|
||||
|
||||
this.watcher = fs.watch(
|
||||
this.basePath,
|
||||
{ recursive: true },
|
||||
(eventType, filename) => {
|
||||
if (filename === null || filename.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.watcher = new Watcher(this.basePath, {
|
||||
recursive: true,
|
||||
renameDetection: true,
|
||||
renameTimeout: 125,
|
||||
ignoreInitial: true
|
||||
});
|
||||
|
||||
// Convert to forward slashes for consistency
|
||||
const relativePath = this.toUnixPath(filename);
|
||||
this.watcher.on("add", (filePath: string) => {
|
||||
this.handleCreate(this.toRelativePath(filePath));
|
||||
});
|
||||
|
||||
if (eventType === "rename") {
|
||||
this.handleRenameOrDelete(relativePath);
|
||||
} else {
|
||||
// Must be "change" event
|
||||
this.handleChange(relativePath);
|
||||
}
|
||||
}
|
||||
this.watcher.on("change", (filePath: string) => {
|
||||
this.handleChange(this.toRelativePath(filePath));
|
||||
});
|
||||
|
||||
this.watcher.on("unlink", (filePath: string) => {
|
||||
this.handleDelete(this.toRelativePath(filePath));
|
||||
});
|
||||
|
||||
this.watcher.on("rename", (oldPath: string, newPath: string) => {
|
||||
this.handleRename(
|
||||
this.toRelativePath(oldPath),
|
||||
this.toRelativePath(newPath)
|
||||
);
|
||||
});
|
||||
|
||||
this.client.logger.info("File watcher started");
|
||||
}
|
||||
|
|
@ -50,44 +56,53 @@ export class FileWatcher {
|
|||
this.client.logger.info("File watcher stopped");
|
||||
}
|
||||
|
||||
private handleCreate(relativePath: RelativePath): void {
|
||||
this.client
|
||||
.syncLocallyCreatedFile(relativePath)
|
||||
.catch((err: unknown) => {
|
||||
this.client.logger.error(
|
||||
`Failed to sync created file ${relativePath}: ${this.formatError(err)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private handleChange(relativePath: RelativePath): void {
|
||||
this.client
|
||||
.syncLocallyUpdatedFile({ relativePath })
|
||||
.catch((err: unknown) => {
|
||||
this.client.logger.error(
|
||||
`Failed to sync updated file ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
|
||||
`Failed to sync updated file ${relativePath}: ${this.formatError(err)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private handleRenameOrDelete(relativePath: RelativePath): void {
|
||||
const fullPath = path.join(this.basePath, relativePath);
|
||||
|
||||
fs.access(fullPath, fs.constants.F_OK, (accessError) => {
|
||||
if (accessError) {
|
||||
private handleDelete(relativePath: RelativePath): void {
|
||||
this.client
|
||||
.syncLocallyDeletedFile(relativePath)
|
||||
.catch((deleteErr: unknown) => {
|
||||
.catch((err: unknown) => {
|
||||
this.client.logger.error(
|
||||
`Failed to sync deleted file ${relativePath}: ${deleteErr instanceof Error ? deleteErr.message : String(deleteErr)}`
|
||||
`Failed to sync deleted file ${relativePath}: ${this.formatError(err)}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
fs.stat(fullPath, (statErr, stats) => {
|
||||
if (statErr !== null || !stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleRename(oldPath: RelativePath, newPath: RelativePath): void {
|
||||
this.client.logger.info(`File renamed: ${oldPath} -> ${newPath}`);
|
||||
this.client
|
||||
.syncLocallyCreatedFile(relativePath)
|
||||
.catch((createErr: unknown) => {
|
||||
.syncLocallyUpdatedFile({
|
||||
oldPath,
|
||||
relativePath: newPath
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
this.client.logger.error(
|
||||
`Failed to sync created file ${relativePath}: ${createErr instanceof Error ? createErr.message : String(createErr)}`
|
||||
`Failed to sync renamed file ${oldPath} -> ${newPath}: ${this.formatError(err)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
private toRelativePath(absolutePath: string): RelativePath {
|
||||
const relative = path.relative(this.basePath, absolutePath);
|
||||
return this.toUnixPath(relative);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,4 +114,8 @@ export class FileWatcher {
|
|||
}
|
||||
return nativePath;
|
||||
}
|
||||
|
||||
private formatError(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@
|
|||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": ["dist"]
|
||||
"exclude": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -85,8 +85,3 @@ If you have multiple URLs, you can also do:
|
|||
## API Documentation
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -136,10 +136,7 @@ export default class VaultLinkPlugin extends Plugin {
|
|||
...(IS_DEBUG_BUILD
|
||||
? {
|
||||
fetch: debugging.slowFetchFactory(1),
|
||||
webSocket: debugging.slowWebSocketFactory(
|
||||
1,
|
||||
new Logger()
|
||||
)
|
||||
webSocket: debugging.slowWebSocketFactory(1, new Logger())
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
|
@ -174,7 +171,7 @@ export default class VaultLinkPlugin extends Plugin {
|
|||
|
||||
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
|
||||
|
||||
client.addRemoteCursorsUpdateListener((cursors) => {
|
||||
client.onRemoteCursorsUpdated.add((cursors) => {
|
||||
RemoteCursorsPluginValue.setCursors(cursors, this.app);
|
||||
renderCursorsInFileExplorer(cursors, this.app);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class HistoryView extends ItemView {
|
|||
super(leaf);
|
||||
this.icon = HistoryView.ICON;
|
||||
|
||||
this.client.addSyncHistoryUpdateListener(async () =>
|
||||
this.client.onSyncHistoryUpdated.add(async () =>
|
||||
this.updateView().catch((error: unknown) => {
|
||||
this.client.logger.error(
|
||||
`Failed to update history view: ${error}`
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class LogsView extends ItemView {
|
|||
) {
|
||||
super(leaf);
|
||||
this.icon = LogsView.ICON;
|
||||
this.client.logger.addOnMessageListener(() => {
|
||||
this.client.logger.onLogEmitted.add(() => {
|
||||
this.updateView();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,40 @@
|
|||
height: 75px;
|
||||
}
|
||||
|
||||
.ignored-files-container {
|
||||
margin-top: var(--size-4-3);
|
||||
padding: var(--size-4-3);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-secondary);
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--size-4-2);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.ignored-files-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding-left: var(--size-4-4);
|
||||
|
||||
li {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
padding: var(--size-2-1) 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.applying-changes-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { App } from "obsidian";
|
|||
import { Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import type VaultLinkPlugin from "src/vault-link-plugin";
|
||||
import type { SyncClient, SyncSettings } from "sync-client";
|
||||
import { globsToRegexes } from "sync-client";
|
||||
import { HistoryView } from "../history/history-view";
|
||||
import { LogsView } from "../logs/logs-view";
|
||||
import type { StatusDescription } from "../status-description/status-description";
|
||||
|
|
@ -41,8 +42,7 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
this.editedToken = this.syncClient.getSettings().token;
|
||||
this.editedVaultName = this.syncClient.getSettings().vaultName;
|
||||
|
||||
this.syncClient.addOnSettingsChangeListener(
|
||||
(newSettings, oldSettings) => {
|
||||
this.syncClient.onSettingsChanged.add((newSettings, oldSettings) => {
|
||||
let hasChanged = false;
|
||||
|
||||
if (newSettings.remoteUri !== oldSettings.remoteUri) {
|
||||
|
|
@ -63,8 +63,7 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
if (hasChanged) {
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private get isApplyingChanges(): boolean {
|
||||
|
|
@ -353,6 +352,55 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Update tracked files")
|
||||
.setDesc(
|
||||
"Apply current ignore patterns to already tracked files. Files that now match ignore patterns will be deleted from sync, and files that no longer match will be added to sync."
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Update tracked files")
|
||||
.setDisabled(this.isApplyingChanges)
|
||||
.setTooltip(
|
||||
this.isApplyingChanges
|
||||
? "Waiting for applying changes to finish..."
|
||||
: "Update tracked files based on current ignore patterns"
|
||||
)
|
||||
.onClick(() => {
|
||||
void (async (): Promise<void> => {
|
||||
await this.updateTrackedFilesBasedOnIgnorePatterns();
|
||||
})();
|
||||
})
|
||||
);
|
||||
|
||||
const ignoredFilesContainer = containerEl.createDiv({
|
||||
cls: "ignored-files-container"
|
||||
});
|
||||
let isIgnoredFilesVisible = false;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Show ignored files")
|
||||
.setDesc(
|
||||
"Display a list of all files currently ignored by the patterns above."
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Show ignored files").onClick(() => {
|
||||
void (async (): Promise<void> => {
|
||||
if (isIgnoredFilesVisible) {
|
||||
ignoredFilesContainer.empty();
|
||||
isIgnoredFilesVisible = false;
|
||||
button.setButtonText("Show ignored files");
|
||||
} else {
|
||||
await this.displayIgnoredFiles(
|
||||
ignoredFilesContainer
|
||||
);
|
||||
isIgnoredFilesVisible = true;
|
||||
button.setButtonText("Hide ignored files");
|
||||
}
|
||||
})();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sync concurrency")
|
||||
.setDesc(
|
||||
|
|
@ -564,4 +612,136 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
|
||||
return [titleContainer, updateTitle];
|
||||
}
|
||||
|
||||
private async updateTrackedFilesBasedOnIgnorePatterns(): Promise<void> {
|
||||
this.isApplyingChanges = true;
|
||||
try {
|
||||
const ignorePatterns: RegExp[] = globsToRegexes(
|
||||
this.syncClient.getSettings().ignorePatterns,
|
||||
this.syncClient.logger
|
||||
);
|
||||
|
||||
const matchesIgnorePattern = (path: string): boolean => {
|
||||
return ignorePatterns.some((pattern) => pattern.test(path));
|
||||
};
|
||||
|
||||
const trackedFiles: string[] =
|
||||
this.syncClient.getTrackedFilePaths();
|
||||
const allVaultFiles: string[] =
|
||||
await this.syncClient.getAllVaultFiles();
|
||||
|
||||
const filesToDelete: string[] = trackedFiles.filter((path) =>
|
||||
matchesIgnorePattern(path)
|
||||
);
|
||||
const filesToCreate: string[] = allVaultFiles.filter(
|
||||
(path) =>
|
||||
!matchesIgnorePattern(path) && !trackedFiles.includes(path)
|
||||
);
|
||||
|
||||
if (filesToDelete.length === 0 && filesToCreate.length === 0) {
|
||||
new Notice("No files need to be updated");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessageParts: string[] = [
|
||||
`This will update ${filesToDelete.length + filesToCreate.length} file(s):`
|
||||
];
|
||||
|
||||
if (filesToDelete.length > 0) {
|
||||
confirmMessageParts.push(
|
||||
`- Delete ${filesToDelete.length} file(s) from sync (now ignored)`
|
||||
);
|
||||
}
|
||||
|
||||
if (filesToCreate.length > 0) {
|
||||
confirmMessageParts.push(
|
||||
`- Add ${filesToCreate.length} file(s) to sync (no longer ignored)`
|
||||
);
|
||||
}
|
||||
|
||||
confirmMessageParts.push("", "Do you want to continue?");
|
||||
|
||||
const confirmMessage = confirmMessageParts.join("\n");
|
||||
|
||||
const confirmed = confirm(confirmMessage);
|
||||
if (!confirmed) {
|
||||
new Notice("Update cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(
|
||||
`Updating ${filesToDelete.length + filesToCreate.length} file(s)...`
|
||||
);
|
||||
|
||||
for (const path of filesToDelete) {
|
||||
await this.syncClient.syncLocallyDeletedFile(path);
|
||||
}
|
||||
|
||||
for (const path of filesToCreate) {
|
||||
await this.syncClient.syncLocallyCreatedFile(path);
|
||||
}
|
||||
|
||||
new Notice(
|
||||
`Successfully updated ${filesToDelete.length + filesToCreate.length} file(s)`
|
||||
);
|
||||
} catch (error) {
|
||||
new Notice(`Error updating tracked files: ${String(error)}`);
|
||||
this.syncClient.logger.error(
|
||||
`Error updating tracked files: ${String(error)}`
|
||||
);
|
||||
} finally {
|
||||
this.isApplyingChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async displayIgnoredFiles(container: HTMLElement): Promise<void> {
|
||||
container.empty();
|
||||
|
||||
try {
|
||||
const ignorePatterns: RegExp[] = globsToRegexes(
|
||||
this.syncClient.getSettings().ignorePatterns,
|
||||
this.syncClient.logger
|
||||
);
|
||||
|
||||
if (ignorePatterns.length === 0) {
|
||||
container.createEl("p", {
|
||||
text: "No ignore patterns configured"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const allVaultFiles: string[] =
|
||||
await this.syncClient.getAllVaultFiles();
|
||||
const ignoredFiles: string[] = allVaultFiles.filter((path) =>
|
||||
ignorePatterns.some((pattern) => pattern.test(path))
|
||||
);
|
||||
|
||||
if (ignoredFiles.length === 0) {
|
||||
container.createEl("p", {
|
||||
text: "No files are currently ignored"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
container.createEl("h4", {
|
||||
text: `Ignored files (${ignoredFiles.length})`
|
||||
});
|
||||
|
||||
const fileList = container.createEl("ul", {
|
||||
cls: "ignored-files-list"
|
||||
});
|
||||
|
||||
const sortedIgnoredFiles = [...ignoredFiles].sort();
|
||||
for (const path of sortedIgnoredFiles) {
|
||||
fileList.createEl("li", { text: path });
|
||||
}
|
||||
} catch (error) {
|
||||
container.createEl("p", {
|
||||
text: `Error loading ignored files: ${String(error)}`
|
||||
});
|
||||
this.syncClient.logger.error(
|
||||
`Error loading ignored files: ${String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@ export class StatusBar {
|
|||
private readonly syncClient: SyncClient
|
||||
) {
|
||||
this.statusBarItem = plugin.addStatusBarItem();
|
||||
this.syncClient.addSyncHistoryUpdateListener((status) => {
|
||||
this.syncClient.onSyncHistoryUpdated.add((status) => {
|
||||
this.lastHistoryStats = status;
|
||||
this.updateStatus();
|
||||
});
|
||||
|
||||
this.syncClient.addRemainingSyncOperationsListener(
|
||||
this.syncClient.onRemainingOperationsCountChanged.add(
|
||||
(remainingOperations) => {
|
||||
this.lastRemaining = remainingOperations;
|
||||
this.updateStatus();
|
||||
}
|
||||
);
|
||||
|
||||
this.syncClient.addOnSettingsChangeListener(() => {
|
||||
this.syncClient.onSettingsChanged.add(() => {
|
||||
this.updateStatus();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,35 @@ import type {
|
|||
NetworkConnectionStatus,
|
||||
SyncClient
|
||||
} from "sync-client";
|
||||
import { utils } from "sync-client";
|
||||
|
||||
export class StatusDescription {
|
||||
private lastHistoryStats: HistoryStats | undefined;
|
||||
private lastRemaining: number | undefined;
|
||||
private lastConnectionState: NetworkConnectionStatus | undefined;
|
||||
|
||||
private statusChangeListeners: (() => unknown)[] = [];
|
||||
private readonly statusChangeListeners: (() => unknown)[] = [];
|
||||
|
||||
public constructor(private readonly syncClient: SyncClient) {
|
||||
void this.updateConnectionState();
|
||||
|
||||
syncClient.addSyncHistoryUpdateListener((status) => {
|
||||
syncClient.onSyncHistoryUpdated.add((status) => {
|
||||
this.lastHistoryStats = status;
|
||||
this.updateDescription();
|
||||
});
|
||||
|
||||
this.syncClient.addRemainingSyncOperationsListener(
|
||||
this.syncClient.onRemainingOperationsCountChanged.add(
|
||||
(remainingOperations) => {
|
||||
this.lastRemaining = remainingOperations;
|
||||
this.updateDescription();
|
||||
}
|
||||
);
|
||||
|
||||
this.syncClient.addWebSocketStatusChangeListener(async () =>
|
||||
this.syncClient.onWebSocketStatusChanged.add(async () =>
|
||||
this.updateConnectionState()
|
||||
);
|
||||
|
||||
this.syncClient.addOnSettingsChangeListener(async () =>
|
||||
this.syncClient.onSettingsChanged.add(async () =>
|
||||
this.updateConnectionState()
|
||||
);
|
||||
}
|
||||
|
|
@ -46,9 +47,7 @@ export class StatusDescription {
|
|||
this.statusChangeListeners.push(listener);
|
||||
}
|
||||
public removeStatusChangeListener(listener: () => unknown): void {
|
||||
this.statusChangeListeners = this.statusChangeListeners.filter(
|
||||
(l) => l !== listener
|
||||
);
|
||||
utils.removeFromArray(this.statusChangeListeners, listener);
|
||||
}
|
||||
|
||||
public renderStatusDescription(container: HTMLElement): void {
|
||||
|
|
|
|||
2849
frontend/package-lock.json
generated
2849
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@
|
|||
"prettier": {
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
"eclint": "^2.8.1",
|
||||
"eslint": "9.38.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"npm-check-updates": "^19.1.1",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"scripts": {
|
||||
"dev": "webpack watch --mode development",
|
||||
"build": "webpack --mode production",
|
||||
"test": "tsx --test src/**/*.test.ts"
|
||||
"test": "tsx --test 'src/**/*.test.ts'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"byte-base64": "^1.1.0",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { slowWebSocketFactory } from "./utils/debugging/slow-web-socket-factory"
|
|||
import { getRandomColor } from "./utils/get-random-color";
|
||||
import { lineAndColumnToPosition } from "./utils/line-and-column-to-position";
|
||||
import { positionToLineAndColumn } from "./utils/position-to-line-and-column";
|
||||
import { removeFromArray } from "./utils/remove-from-array";
|
||||
|
||||
export {
|
||||
SyncType,
|
||||
|
|
@ -39,9 +40,12 @@ export const debugging = {
|
|||
logToConsole
|
||||
};
|
||||
|
||||
export { globsToRegexes } from "./utils/globs-to-regexes";
|
||||
|
||||
export const utils = {
|
||||
getRandomColor,
|
||||
positionToLineAndColumn,
|
||||
lineAndColumnToPosition,
|
||||
awaitAll
|
||||
awaitAll,
|
||||
removeFromArray
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Logger } from "../tracing/logger";
|
|||
import { EMPTY_HASH } from "../utils/hash";
|
||||
import { CoveredValues } from "../utils/data-structures/min-covered";
|
||||
import { awaitAll } from "../utils/await-all";
|
||||
import { removeFromArray } from "../utils/remove-from-array";
|
||||
|
||||
export type VaultUpdateId = number;
|
||||
export type DocumentId = string;
|
||||
|
|
@ -93,6 +94,7 @@ export class Database {
|
|||
public get resolvedDocuments(): DocumentRecord[] {
|
||||
const paths = new Map<string, DocumentRecord[]>();
|
||||
this.documents
|
||||
// eslint-disable-next-line no-restricted-syntax -- Type narrowing, not removing a specific item
|
||||
.filter(({ metadata }) => metadata !== undefined)
|
||||
.forEach((record) =>
|
||||
paths.set(record.relativePath, [
|
||||
|
|
@ -151,12 +153,12 @@ export class Database {
|
|||
return;
|
||||
}
|
||||
|
||||
entry.updates = entry.updates.filter((update) => update !== promise);
|
||||
removeFromArray(entry.updates, promise);
|
||||
// No need to save as Promises don't get serialized
|
||||
}
|
||||
|
||||
public removeDocument(find: DocumentRecord): void {
|
||||
this.documents = this.documents.filter((document) => document !== find);
|
||||
removeFromArray(this.documents, find);
|
||||
this.saveInTheBackground();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Logger } from "../tracing/logger";
|
||||
import { awaitAll } from "../utils/await-all";
|
||||
import { Lock } from "../utils/data-structures/locks";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
export interface SyncSettings {
|
||||
remoteUri: string;
|
||||
|
|
@ -33,14 +33,13 @@ export const DEFAULT_SETTINGS: SyncSettings = {
|
|||
};
|
||||
|
||||
export class Settings {
|
||||
public readonly onSettingsChanged = new EventListeners<
|
||||
(newSettings: SyncSettings, oldSettings: SyncSettings) => unknown
|
||||
>();
|
||||
|
||||
private settings: SyncSettings;
|
||||
private readonly lock: Lock = new Lock();
|
||||
|
||||
private readonly onSettingsChangeHandlers: ((
|
||||
newSettings: SyncSettings,
|
||||
oldSettings: SyncSettings
|
||||
) => unknown)[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly logger: Logger,
|
||||
initialState: Partial<SyncSettings> | undefined,
|
||||
|
|
@ -60,21 +59,6 @@ export class Settings {
|
|||
return this.settings;
|
||||
}
|
||||
|
||||
public addOnSettingsChangeListener(
|
||||
listener: (settings: SyncSettings, oldSettings: SyncSettings) => unknown
|
||||
): void {
|
||||
this.onSettingsChangeHandlers.push(listener);
|
||||
}
|
||||
|
||||
public removeOnSettingsChangeListener(
|
||||
listener: (settings: SyncSettings, oldSettings: SyncSettings) => unknown
|
||||
): void {
|
||||
const index = this.onSettingsChangeHandlers.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this.onSettingsChangeHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public async setSetting<T extends keyof SyncSettings>(
|
||||
key: T,
|
||||
value: SyncSettings[T]
|
||||
|
|
@ -95,14 +79,9 @@ export class Settings {
|
|||
...value
|
||||
};
|
||||
|
||||
await awaitAll(
|
||||
this.onSettingsChangeHandlers
|
||||
.map((handler) => {
|
||||
return handler(this.settings, oldSettings);
|
||||
})
|
||||
.filter((result): result is Promise<unknown> => {
|
||||
return result instanceof Promise;
|
||||
})
|
||||
await this.onSettingsChanged.triggerAsync(
|
||||
this.settings,
|
||||
oldSettings
|
||||
);
|
||||
|
||||
await this.save();
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ describe("WebSocketManager", () => {
|
|||
MockWebSocket as unknown as typeof WebSocket
|
||||
);
|
||||
|
||||
manager.addRemoteVaultUpdateListener(async () => {
|
||||
manager.onRemoteVaultUpdateReceived.add(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
manager.start();
|
||||
|
|
@ -152,7 +152,7 @@ describe("WebSocketManager", () => {
|
|||
MockWebSocket as unknown as typeof WebSocket
|
||||
);
|
||||
|
||||
manager.addRemoteCursorsUpdateListener(async () => {
|
||||
manager.onRemoteCursorsUpdateReceived.add(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
manager.start();
|
||||
|
|
@ -227,7 +227,7 @@ describe("WebSocketManager", () => {
|
|||
);
|
||||
|
||||
let statusChangeCount = 0;
|
||||
manager.addWebSocketStatusChangeListener(() => {
|
||||
manager.onWebSocketStatusChanged.add(() => {
|
||||
statusChangeCount++;
|
||||
});
|
||||
|
||||
|
|
@ -269,7 +269,7 @@ describe("WebSocketManager", () => {
|
|||
resolveListener = resolve;
|
||||
});
|
||||
|
||||
manager.addRemoteVaultUpdateListener(async () => {
|
||||
manager.onRemoteVaultUpdateReceived.add(async () => {
|
||||
await listenerPromise;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,21 +6,23 @@ import type { CursorPositionFromClient } from "./types/CursorPositionFromClient"
|
|||
import type { ClientCursors } from "./types/ClientCursors";
|
||||
import { createPromise } from "../utils/create-promise";
|
||||
import type { WebSocketVaultUpdate } from "./types/WebSocketVaultUpdate";
|
||||
import { awaitAll } from "../utils/await-all";
|
||||
import { WEBSOCKET_DISCONNECT_TIMEOUT_IN_S } from "../consts";
|
||||
import { removeFromArray } from "../utils/remove-from-array";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
import { awaitAll } from "../utils/await-all";
|
||||
|
||||
export class WebSocketManager {
|
||||
private readonly webSocketStatusChangeListeners: ((
|
||||
isConnected: boolean
|
||||
) => unknown)[] = [];
|
||||
public readonly onWebSocketStatusChanged = new EventListeners<
|
||||
(isConnected: boolean) => unknown
|
||||
>();
|
||||
|
||||
private readonly remoteVaultUpdateListeners: ((
|
||||
update: WebSocketVaultUpdate
|
||||
) => Promise<void>)[] = [];
|
||||
public readonly onRemoteVaultUpdateReceived = new EventListeners<
|
||||
(update: WebSocketVaultUpdate) => Promise<void>
|
||||
>();
|
||||
|
||||
private readonly remoteCursorsUpdateListeners: ((
|
||||
cursors: ClientCursors[]
|
||||
) => Promise<void>)[] = [];
|
||||
public readonly onRemoteCursorsUpdateReceived = new EventListeners<
|
||||
(cursors: ClientCursors[]) => Promise<void>
|
||||
>();
|
||||
|
||||
private isStopped = true;
|
||||
private resolveDisconnectingPromise: null | (() => unknown) = null;
|
||||
|
|
@ -59,24 +61,6 @@ export class WebSocketManager {
|
|||
);
|
||||
}
|
||||
|
||||
public addWebSocketStatusChangeListener(
|
||||
listener: (isConnected: boolean) => unknown
|
||||
): void {
|
||||
this.webSocketStatusChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
public addRemoteCursorsUpdateListener(
|
||||
listener: (cursors: ClientCursors[]) => Promise<void>
|
||||
): void {
|
||||
this.remoteCursorsUpdateListeners.push(listener);
|
||||
}
|
||||
|
||||
public addRemoteVaultUpdateListener(
|
||||
listener: (update: WebSocketVaultUpdate) => Promise<void>
|
||||
): void {
|
||||
this.remoteVaultUpdateListeners.push(listener);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.isStopped = false;
|
||||
this.initializeWebSocket();
|
||||
|
|
@ -205,9 +189,7 @@ export class WebSocketManager {
|
|||
|
||||
this.webSocket.onopen = (): void => {
|
||||
this.logger.info("WebSocket connection opened");
|
||||
this.webSocketStatusChangeListeners.forEach((listener) =>
|
||||
listener(true)
|
||||
);
|
||||
this.onWebSocketStatusChanged.trigger(true);
|
||||
};
|
||||
|
||||
this.webSocket.onmessage = (event): void => {
|
||||
|
|
@ -227,12 +209,10 @@ export class WebSocketManager {
|
|||
);
|
||||
})
|
||||
.finally(() => {
|
||||
const index = this.outstandingPromises.indexOf(
|
||||
removeFromArray(
|
||||
this.outstandingPromises,
|
||||
messageHandlingPromise
|
||||
);
|
||||
if (index !== -1) {
|
||||
void this.outstandingPromises.splice(index, 1); // ignore the returned promise
|
||||
}
|
||||
});
|
||||
|
||||
void this.outstandingPromises.push(messageHandlingPromise); // ignore the returned promise
|
||||
|
|
@ -247,9 +227,7 @@ export class WebSocketManager {
|
|||
this.logger.warn(
|
||||
`WebSocket closed with code ${event.code} (${event.reason == "" ? "unknown reason" : event.reason})`
|
||||
);
|
||||
this.webSocketStatusChangeListeners.forEach((listener) =>
|
||||
listener(false)
|
||||
);
|
||||
this.onWebSocketStatusChanged.trigger(false);
|
||||
|
||||
if (this.isStopped) {
|
||||
this.resolveDisconnectingPromise?.();
|
||||
|
|
@ -267,15 +245,7 @@ export class WebSocketManager {
|
|||
message: WebSocketServerMessage
|
||||
): Promise<void> {
|
||||
if (message.type === "vaultUpdate") {
|
||||
await awaitAll(
|
||||
this.remoteVaultUpdateListeners.map(async (listener) => {
|
||||
await listener(message).catch((error: unknown) => {
|
||||
this.logger.error(
|
||||
`Error in vault update listener: ${String(error)}`
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
await this.onRemoteVaultUpdateReceived.triggerAsync(message);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (message.type === "cursorPositions") {
|
||||
|
|
@ -283,14 +253,8 @@ export class WebSocketManager {
|
|||
`Received cursor positions for ${JSON.stringify(message.clients)}`
|
||||
);
|
||||
|
||||
await awaitAll(
|
||||
this.remoteCursorsUpdateListeners.map(async (listener) => {
|
||||
await listener(message.clients).catch((error: unknown) => {
|
||||
this.logger.error(
|
||||
`Error in cursor positions listener: ${String(error)}`
|
||||
);
|
||||
});
|
||||
})
|
||||
await this.onRemoteCursorsUpdateReceived.triggerAsync(
|
||||
message.clients
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { FixedSizeDocumentCache } from "./utils/data-structures/fix-sized-cache"
|
|||
import { setUpTelemetry } from "./utils/set-up-telemetry";
|
||||
import { DIFF_CACHE_SIZE_MB } from "./consts";
|
||||
import { ServerConfig } from "./services/server-config";
|
||||
import type { EventListeners } from "./utils/data-structures/event-listeners";
|
||||
|
||||
export class SyncClient {
|
||||
private hasStartedOfflineSync = false;
|
||||
|
|
@ -62,6 +63,42 @@ export class SyncClient {
|
|||
public get isWebSocketConnected(): boolean {
|
||||
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({
|
||||
fs,
|
||||
persistence,
|
||||
|
|
@ -122,7 +159,7 @@ export class SyncClient {
|
|||
settings.getSettings().isSyncEnabled,
|
||||
logger
|
||||
);
|
||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||
settings.onSettingsChanged.add((newSettings, oldSettings) => {
|
||||
if (oldSettings.isSyncEnabled != newSettings.isSyncEnabled) {
|
||||
fetchController.canFetch = newSettings.isSyncEnabled;
|
||||
}
|
||||
|
|
@ -221,15 +258,13 @@ export class SyncClient {
|
|||
this.unloadTelemetry = setUpTelemetry();
|
||||
}
|
||||
|
||||
this.logger.addOnMessageListener((log): void => {
|
||||
this.logger.onLogEmitted.add((log): void => {
|
||||
if (log.level === LogLevel.ERROR && Sentry.isInitialized()) {
|
||||
Sentry.captureMessage(log.message);
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.addOnSettingsChangeListener(
|
||||
this.onSettingsChange.bind(this)
|
||||
);
|
||||
this.settings.onSettingsChanged.add(this.onSettingsChange.bind(this));
|
||||
|
||||
if (this.settings.getSettings().isSyncEnabled) {
|
||||
this.logger.info("Starting SyncClient");
|
||||
|
|
@ -273,14 +308,6 @@ export class SyncClient {
|
|||
return this.history.entries;
|
||||
}
|
||||
|
||||
public addSyncHistoryUpdateListener(
|
||||
listener: (stats: HistoryStats) => unknown
|
||||
): void {
|
||||
this.checkIfDestroyed("addSyncHistoryUpdateListener");
|
||||
|
||||
this.history.addSyncHistoryUpdateListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the in-flight operations to finish, reset all tracking,
|
||||
* and the local database but retain the settings.
|
||||
|
|
@ -325,28 +352,6 @@ export class SyncClient {
|
|||
await this.settings.setSettings(value);
|
||||
}
|
||||
|
||||
public addOnSettingsChangeListener(
|
||||
listener: (settings: SyncSettings, oldSettings: SyncSettings) => unknown
|
||||
): void {
|
||||
this.checkIfDestroyed("addOnSettingsChangeListener");
|
||||
|
||||
this.settings.addOnSettingsChangeListener(listener);
|
||||
}
|
||||
|
||||
public addRemainingSyncOperationsListener(
|
||||
listener: (remainingOperations: number) => unknown
|
||||
): void {
|
||||
this.checkIfDestroyed("addRemainingSyncOperationsListener");
|
||||
|
||||
this.syncer.addRemainingOperationsListener(listener);
|
||||
}
|
||||
|
||||
public addWebSocketStatusChangeListener(listener: () => unknown): void {
|
||||
this.checkIfDestroyed("addWebSocketStatusChangeListener");
|
||||
|
||||
this.webSocketManager.addWebSocketStatusChangeListener(listener);
|
||||
}
|
||||
|
||||
public async syncLocallyCreatedFile(
|
||||
relativePath: RelativePath
|
||||
): Promise<void> {
|
||||
|
|
@ -412,12 +417,18 @@ export class SyncClient {
|
|||
await this.cursorTracker.sendLocalCursorsToServer(documentToCursors);
|
||||
}
|
||||
|
||||
public addRemoteCursorsUpdateListener(
|
||||
listener: (cursors: MaybeOutdatedClientCursors[]) => unknown
|
||||
): void {
|
||||
this.checkIfDestroyed("addRemoteCursorsUpdateListener");
|
||||
public getTrackedFilePaths(): RelativePath[] {
|
||||
this.checkIfDestroyed("getTrackedFilePaths");
|
||||
|
||||
this.cursorTracker.addRemoteCursorsUpdateListener(listener);
|
||||
return this.database.resolvedDocuments
|
||||
.filter((doc) => !doc.isDeleted && doc.metadata !== undefined)
|
||||
.map((doc) => doc.relativePath);
|
||||
}
|
||||
|
||||
public async getAllVaultFiles(): Promise<RelativePath[]> {
|
||||
this.checkIfDestroyed("getAllVaultFiles");
|
||||
|
||||
return this.fileOperations.listFilesRecursively(undefined);
|
||||
}
|
||||
|
||||
public async waitUntilFinished(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,19 @@ import { DocumentUpToDateness } from "../types/document-up-to-dateness";
|
|||
import { hash } from "../utils/hash";
|
||||
import type { FileChangeNotifier } from "./file-change-notifier";
|
||||
import { Lock } from "../utils/data-structures/locks";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
// Cursor positions are updated separately from documents. However, a given cursor position is only
|
||||
// valid within a certain version of the document it belongs to. This class tracks previous and the latest
|
||||
// known remote cursor positions, and for each document, tries to return the latest cursor positions that are
|
||||
// not from the future.
|
||||
export class CursorTracker {
|
||||
// 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.
|
||||
public readonly onRemoteCursorsUpdated = new EventListeners<
|
||||
(cursors: MaybeOutdatedClientCursors[]) => unknown
|
||||
>();
|
||||
|
||||
private readonly updateLock = new Lock();
|
||||
|
||||
private knownRemoteCursors: (ClientCursors & {
|
||||
|
|
@ -31,7 +38,7 @@ export class CursorTracker {
|
|||
private readonly fileOperations: FileOperations,
|
||||
private readonly fileChangeNotifier: FileChangeNotifier
|
||||
) {
|
||||
this.webSocketManager.addRemoteCursorsUpdateListener(
|
||||
this.webSocketManager.onRemoteCursorsUpdateReceived.add(
|
||||
async (clientCursors) => {
|
||||
await this.updateLock.withLock(async () => {
|
||||
// The latest message will contain all active clients, so we can delete the ones
|
||||
|
|
@ -58,10 +65,14 @@ export class CursorTracker {
|
|||
|
||||
this.knownRemoteCursors = updatedKnownRemoteCursors;
|
||||
});
|
||||
|
||||
this.onRemoteCursorsUpdated.trigger(
|
||||
this.getRelevantAndPruneKnownClientCursors()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.fileChangeNotifier.addFileChangeListener(async (relativePath) =>
|
||||
this.fileChangeNotifier.onFileChanged.add(async (relativePath) =>
|
||||
this.updateLock.withLock(async () => {
|
||||
for (const clientCursor of this.knownRemoteCursors) {
|
||||
if (
|
||||
|
|
@ -144,19 +155,6 @@ export class CursorTracker {
|
|||
this.webSocketManager.updateLocalCursors({ documentsWithCursors });
|
||||
}
|
||||
|
||||
// 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.
|
||||
public addRemoteCursorsUpdateListener(
|
||||
listener: (cursors: MaybeOutdatedClientCursors[]) => unknown
|
||||
): void {
|
||||
// CursorTracker registers its own event listener in the constructor so it must have been called before this
|
||||
this.webSocketManager.addRemoteCursorsUpdateListener(async () => {
|
||||
await this.updateLock.withLock(() =>
|
||||
listener(this.getRelevantAndPruneKnownClientCursors())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.knownRemoteCursors = [];
|
||||
this.lastLocalCursorState = [];
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
import type { RelativePath } from "../persistence/database";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
export class FileChangeNotifier {
|
||||
private readonly listeners: ((filePath: RelativePath) => unknown)[] = [];
|
||||
|
||||
public addFileChangeListener(
|
||||
listener: (filePath: RelativePath) => unknown
|
||||
): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
public removeFileChangeListener(
|
||||
listener: (filePath: RelativePath) => unknown
|
||||
): void {
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
public readonly onFileChanged = new EventListeners<
|
||||
(filePath: RelativePath) => unknown
|
||||
>();
|
||||
|
||||
public notifyOfFileChange(filePath: RelativePath): void {
|
||||
this.listeners.forEach((listener) => listener(filePath));
|
||||
this.onFileChanged.trigger(filePath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,18 +21,21 @@ import type { WebSocketVaultUpdate } from "../services/types/WebSocketVaultUpdat
|
|||
import type { WebSocketManager } from "../services/websocket-manager";
|
||||
import type { WebSocketClientMessage } from "../services/types/WebSocketClientMessage";
|
||||
import { awaitAll } from "../utils/await-all";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
export class Syncer {
|
||||
public readonly onRemainingOperationsCountChanged = new EventListeners<
|
||||
(remainingOperations: number) => unknown
|
||||
>();
|
||||
|
||||
private readonly remoteDocumentsLock: Locks<DocumentId>;
|
||||
private readonly remainingOperationsListeners: ((
|
||||
remainingOperations: number
|
||||
) => unknown)[] = [];
|
||||
|
||||
// FIFO to limit the number of concurrent sync operations
|
||||
private readonly syncQueue: PQueue;
|
||||
|
||||
private _isFirstSyncComplete = false;
|
||||
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
|
||||
private previousRemainingOperationsCount = 0;
|
||||
|
||||
public constructor(
|
||||
private readonly deviceId: string,
|
||||
|
|
@ -50,27 +53,28 @@ export class Syncer {
|
|||
|
||||
this.remoteDocumentsLock = new Locks<DocumentId>(this.logger);
|
||||
|
||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||
settings.onSettingsChanged.add((newSettings, oldSettings) => {
|
||||
if (newSettings.syncConcurrency !== oldSettings.syncConcurrency) {
|
||||
this.syncQueue.concurrency = newSettings.syncConcurrency;
|
||||
}
|
||||
});
|
||||
|
||||
this.syncQueue.on("active", () => {
|
||||
this.remainingOperationsListeners.forEach((listener) => {
|
||||
listener(this.syncQueue.size);
|
||||
});
|
||||
if (this.previousRemainingOperationsCount !== this.syncQueue.size) {
|
||||
this.previousRemainingOperationsCount = this.syncQueue.size;
|
||||
this.onRemainingOperationsCountChanged.trigger(
|
||||
this.syncQueue.size
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.webSocketManager.addWebSocketStatusChangeListener(
|
||||
(isConnected) => {
|
||||
this.webSocketManager.onWebSocketStatusChanged.add((isConnected) => {
|
||||
if (isConnected) {
|
||||
// The JS WebSocket API doesn't support setting headers, so we have to send the token as a message
|
||||
this.sendHandshakeMessage();
|
||||
}
|
||||
}
|
||||
);
|
||||
this.webSocketManager.addRemoteVaultUpdateListener(
|
||||
});
|
||||
this.webSocketManager.onRemoteVaultUpdateReceived.add(
|
||||
this.syncRemotelyUpdatedFile.bind(this)
|
||||
);
|
||||
}
|
||||
|
|
@ -79,12 +83,6 @@ export class Syncer {
|
|||
return this._isFirstSyncComplete;
|
||||
}
|
||||
|
||||
public addRemainingOperationsListener(
|
||||
listener: (remainingOperations: number) => unknown
|
||||
): void {
|
||||
this.remainingOperationsListeners.push(listener);
|
||||
}
|
||||
|
||||
public async syncLocallyCreatedFile(
|
||||
relativePath: RelativePath
|
||||
): Promise<void> {
|
||||
|
|
@ -444,11 +442,13 @@ export class Syncer {
|
|||
);
|
||||
if (originalFile !== undefined) {
|
||||
// `originalFile` hasn't been deleted but it got moved instead
|
||||
/* eslint-disable no-restricted-syntax -- Comparing by property, not direct equality */
|
||||
locallyPossiblyDeletedFiles =
|
||||
locallyPossiblyDeletedFiles.filter(
|
||||
(item) =>
|
||||
item.relativePath !== originalFile.relativePath
|
||||
);
|
||||
/* eslint-enable no-restricted-syntax */
|
||||
|
||||
this.logger.debug(
|
||||
`Document '${originalFile.relativePath}' was not found under its current path in the database but was found under a different path (${relativePath}), scheduling sync to move it`
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class UnrestrictedSyncer {
|
|||
this.logger
|
||||
);
|
||||
|
||||
this.settings.addOnSettingsChangeListener((newSettings) => {
|
||||
this.settings.onSettingsChanged.add((newSettings) => {
|
||||
this.ignorePatterns = globsToRegexes(
|
||||
newSettings.ignorePatterns,
|
||||
this.logger
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { MAX_LOG_MESSAGE_COUNT } from "../consts";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = "DEBUG",
|
||||
|
|
@ -23,14 +24,11 @@ export class LogLine {
|
|||
}
|
||||
|
||||
export class Logger {
|
||||
private readonly messages: LogLine[] = [];
|
||||
private readonly onMessageListeners: ((message: LogLine) => unknown)[] = [];
|
||||
public readonly onLogEmitted = new EventListeners<
|
||||
(message: LogLine) => unknown
|
||||
>();
|
||||
|
||||
public constructor(
|
||||
...onMessageListeners: ((message: LogLine) => unknown)[]
|
||||
) {
|
||||
this.onMessageListeners = onMessageListeners;
|
||||
}
|
||||
private readonly messages: LogLine[] = [];
|
||||
|
||||
public debug(message: string): void {
|
||||
this.pushMessage(message, LogLevel.DEBUG);
|
||||
|
|
@ -56,19 +54,6 @@ export class Logger {
|
|||
);
|
||||
}
|
||||
|
||||
public addOnMessageListener(listener: (message: LogLine) => unknown): void {
|
||||
this.onMessageListeners.push(listener);
|
||||
}
|
||||
|
||||
public removeOnMessageListener(
|
||||
listener: (message: LogLine) => unknown
|
||||
): void {
|
||||
const index = this.onMessageListeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this.onMessageListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.messages.length = 0;
|
||||
this.debug("Logger has been reset");
|
||||
|
|
@ -82,8 +67,6 @@ export class Logger {
|
|||
this.messages.shift();
|
||||
}
|
||||
|
||||
this.onMessageListeners.forEach((listener) => {
|
||||
listener(logLine);
|
||||
});
|
||||
this.onLogEmitted.trigger(logLine);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import {
|
|||
} from "../consts";
|
||||
import type { RelativePath } from "../persistence/database";
|
||||
import type { Logger } from "./logger";
|
||||
import { removeFromArray } from "../utils/remove-from-array";
|
||||
import { EventListeners } from "../utils/data-structures/event-listeners";
|
||||
|
||||
export interface SyncCreateDetails {
|
||||
type: SyncType.CREATE;
|
||||
|
|
@ -68,11 +70,11 @@ export interface HistoryStats {
|
|||
}
|
||||
|
||||
export class SyncHistory {
|
||||
private _entries: HistoryEntry[] = [];
|
||||
public readonly onHistoryUpdated = new EventListeners<
|
||||
(status: HistoryStats) => unknown
|
||||
>();
|
||||
|
||||
private readonly syncHistoryUpdateListeners: ((
|
||||
status: HistoryStats
|
||||
) => unknown)[] = [];
|
||||
private readonly _entries: HistoryEntry[] = [];
|
||||
|
||||
private status: HistoryStats = {
|
||||
success: 0,
|
||||
|
|
@ -99,7 +101,7 @@ export class SyncHistory {
|
|||
|
||||
const candidate = this.findSimilarRecentUpdateEntry(historyEntry);
|
||||
if (candidate !== undefined) {
|
||||
this._entries = this._entries.filter((e) => e !== candidate);
|
||||
removeFromArray(this._entries, candidate);
|
||||
}
|
||||
|
||||
// Insert the entry at the beginning
|
||||
|
|
@ -112,31 +114,13 @@ export class SyncHistory {
|
|||
this.updateSuccessCount(historyEntry);
|
||||
}
|
||||
|
||||
public addSyncHistoryUpdateListener(
|
||||
listener: (stats: HistoryStats) => unknown
|
||||
): void {
|
||||
this.syncHistoryUpdateListeners.push(listener);
|
||||
listener({ ...this.status });
|
||||
}
|
||||
|
||||
public removeSyncHistoryUpdateListener(
|
||||
listener: (stats: HistoryStats) => unknown
|
||||
): void {
|
||||
const index = this.syncHistoryUpdateListeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this.syncHistoryUpdateListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._entries.length = 0;
|
||||
this.status = {
|
||||
success: 0,
|
||||
error: 0
|
||||
};
|
||||
this.syncHistoryUpdateListeners.forEach((listener) => {
|
||||
listener(this.status);
|
||||
});
|
||||
this.onHistoryUpdated.trigger(this.status);
|
||||
}
|
||||
|
||||
private findSimilarRecentUpdateEntry(
|
||||
|
|
@ -178,8 +162,6 @@ export class SyncHistory {
|
|||
break;
|
||||
}
|
||||
|
||||
this.syncHistoryUpdateListeners.forEach((listener) => {
|
||||
listener(this.status);
|
||||
});
|
||||
this.onHistoryUpdated.trigger(this.status);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { EventListeners } from "./event-listeners";
|
||||
|
||||
describe("EventListeners", () => {
|
||||
it("should add & remove listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const listener = (): void => {};
|
||||
|
||||
listeners.add(listener);
|
||||
|
||||
assert.strictEqual(listeners.count, 1);
|
||||
|
||||
const removed = listeners.remove(listener);
|
||||
assert.strictEqual(removed, true);
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
});
|
||||
|
||||
it("should remove listeners using unsubscribe function", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const listener = (): void => {};
|
||||
|
||||
const unsubscribe = listeners.add(listener);
|
||||
unsubscribe();
|
||||
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
});
|
||||
|
||||
it("should return false when removing non-existent listener", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const listener = (): void => {};
|
||||
|
||||
const removed = listeners.remove(listener);
|
||||
|
||||
assert.strictEqual(removed, false);
|
||||
});
|
||||
|
||||
it("should handle multiple listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const listener1 = (): void => {};
|
||||
// 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(listener2);
|
||||
listeners.add(listener3);
|
||||
|
||||
assert.strictEqual(listeners.count, 3);
|
||||
|
||||
listeners.remove(listener2);
|
||||
|
||||
assert.strictEqual(listeners.count, 2);
|
||||
});
|
||||
|
||||
it("should trigger all listeners synchronously", () => {
|
||||
const listeners = new EventListeners<(value: string) => void>();
|
||||
const calls: string[] = [];
|
||||
|
||||
listeners.add((value) => calls.push(`listener1-${value}`));
|
||||
listeners.add((value) => calls.push(`listener2-${value}`));
|
||||
|
||||
listeners.trigger("test");
|
||||
|
||||
assert.deepStrictEqual(calls, ["listener1-test", "listener2-test"]);
|
||||
});
|
||||
|
||||
it("should trigger listeners with multiple arguments", () => {
|
||||
const listeners = new EventListeners<
|
||||
(a: number, b: string, c: boolean) => void
|
||||
>();
|
||||
const calls: [number, string, boolean][] = [];
|
||||
|
||||
listeners.add((a, b, c) => calls.push([a, b, c]));
|
||||
listeners.trigger(42, "hello", true);
|
||||
|
||||
assert.deepStrictEqual(calls, [[42, "hello", true]]);
|
||||
});
|
||||
|
||||
it("should not trigger removed listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
let count1 = 0;
|
||||
let count2 = 0;
|
||||
|
||||
const listener1 = (): void => {
|
||||
count1++;
|
||||
};
|
||||
const listener2 = (): void => {
|
||||
count2++;
|
||||
};
|
||||
|
||||
listeners.add(listener1);
|
||||
const unsubscribe = listeners.add(listener2);
|
||||
|
||||
unsubscribe();
|
||||
listeners.trigger();
|
||||
|
||||
assert.strictEqual(count1, 1);
|
||||
assert.strictEqual(count2, 0);
|
||||
});
|
||||
|
||||
it("should trigger all listeners and await promises", async () => {
|
||||
const listeners = new EventListeners<
|
||||
(value: string) => Promise<void> | void
|
||||
>();
|
||||
const results: string[] = [];
|
||||
|
||||
listeners.add(async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
results.push(`async1-${value}`);
|
||||
});
|
||||
|
||||
listeners.add((value) => {
|
||||
results.push(`sync-${value}`);
|
||||
});
|
||||
|
||||
listeners.add(async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
results.push(`async2-${value}`);
|
||||
});
|
||||
|
||||
await listeners.triggerAsync("test");
|
||||
|
||||
assert.ok(results.includes("async1-test"));
|
||||
assert.ok(results.includes("sync-test"));
|
||||
assert.ok(results.includes("async2-test"));
|
||||
assert.strictEqual(results.length, 3);
|
||||
});
|
||||
|
||||
it("should not trigger cleared listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
let called = false;
|
||||
const listener = (): void => {
|
||||
called = true;
|
||||
};
|
||||
|
||||
listeners.add(listener);
|
||||
listeners.clear();
|
||||
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
listeners.trigger();
|
||||
|
||||
assert.strictEqual(called, false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { removeFromArray } from "../remove-from-array";
|
||||
import { awaitAll } from "../await-all";
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
private readonly listeners: TListener[] = [];
|
||||
|
||||
public get count(): number {
|
||||
return this.listeners.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener to the collection.
|
||||
*
|
||||
* @param listener The listener callback to add
|
||||
* @returns An unsubscribe function that removes this listener when called
|
||||
*/
|
||||
public add(listener: TListener): () => void {
|
||||
this.listeners.push(listener);
|
||||
return () => this.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the collection.
|
||||
*
|
||||
* @param listener The listener callback to remove
|
||||
* @returns true if the listener was found and removed, false otherwise
|
||||
*/
|
||||
public remove(listener: TListener): boolean {
|
||||
return removeFromArray(this.listeners, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all listeners synchronously with the provided arguments.
|
||||
* Any returned promises are ignored. Use triggerAsync() to await them.
|
||||
*
|
||||
* @param args The arguments to pass to each listener
|
||||
*/
|
||||
public trigger(...args: Parameters<TListener>): void {
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all listeners and awaits any promises they return.
|
||||
* Synchronous listeners are called immediately, and any async listeners
|
||||
* are awaited in parallel.
|
||||
*
|
||||
* @param args The arguments to pass to each listener
|
||||
*/
|
||||
public async triggerAsync(...args: Parameters<TListener>): Promise<void> {
|
||||
await awaitAll(
|
||||
this.listeners
|
||||
.map((listener) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return listener(...args);
|
||||
})
|
||||
.filter((result): result is Promise<unknown> => {
|
||||
return result instanceof Promise;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import type { LogLine } from "../../tracing/logger";
|
|||
import { LogLevel } from "../../tracing/logger";
|
||||
|
||||
export function logToConsole(client: SyncClient): void {
|
||||
client.logger.addOnMessageListener((logLine: LogLine) => {
|
||||
client.logger.onLogEmitted.add((logLine: LogLine) => {
|
||||
const formatted = `${logLine.timestamp.toISOString()} ${logLine.level} ${logLine.message}`;
|
||||
|
||||
switch (logLine.level) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { makeRe } from "minimatch";
|
|||
import type { Logger } from "../tracing/logger";
|
||||
|
||||
export function globsToRegexes(globs: string[], logger: Logger): RegExp[] {
|
||||
return globs
|
||||
return (
|
||||
globs
|
||||
.map((pattern) => {
|
||||
const result = makeRe(pattern, {
|
||||
dot: true
|
||||
|
|
@ -14,5 +15,7 @@ export function globsToRegexes(globs: string[], logger: Logger): RegExp[] {
|
|||
}
|
||||
return result;
|
||||
})
|
||||
.filter((pattern) => pattern !== false);
|
||||
// eslint-disable-next-line no-restricted-syntax -- Filtering out false values, not removing a specific item
|
||||
.filter((pattern) => pattern !== false)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
17
frontend/sync-client/src/utils/remove-from-array.ts
Normal file
17
frontend/sync-client/src/utils/remove-from-array.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Efficiently removes a specific item from an array by modifying it in place.
|
||||
* This is more efficient than using `.filter(item => item !== toRemove)` as it avoids creating a new array
|
||||
*
|
||||
* @param array The array to modify
|
||||
* @param item The item to remove
|
||||
* @returns true if the item was found and removed, false otherwise
|
||||
*/
|
||||
export function removeFromArray<T>(array: T[], item: T): boolean {
|
||||
const index = array.indexOf(item);
|
||||
if (index !== -1) {
|
||||
// eslint-disable-next-line no-restricted-syntax -- This is the implementation of the helper itself
|
||||
array.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
"scripts": {
|
||||
"dev": "webpack watch --mode development",
|
||||
"build": "webpack --mode production",
|
||||
"test": "tsx --test src/**/*.test.ts"
|
||||
"test": "tsx --test 'src/**/*.test.ts'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.8.1",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class MockAgent extends MockClient {
|
|||
private readonly pendingActions: Promise<unknown>[] = [];
|
||||
|
||||
// The renamed file finding algorithm isn't too smart so we can't both update and rename the same file
|
||||
private doNotTouchWhileOffline: string[] = [];
|
||||
private readonly doNotTouchWhileOffline: string[] = [];
|
||||
|
||||
public constructor(
|
||||
initialSettings: Partial<SyncSettings>,
|
||||
|
|
@ -42,7 +42,7 @@ export class MockAgent extends MockClient {
|
|||
"Connection check failed"
|
||||
);
|
||||
|
||||
this.client.logger.addOnMessageListener((logLine: LogLine) => {
|
||||
this.client.logger.onLogEmitted.add((logLine: LogLine) => {
|
||||
const state = this.client.getSettings().isSyncEnabled
|
||||
? "(online) "
|
||||
: "(offline)";
|
||||
|
|
@ -54,9 +54,9 @@ export class MockAgent extends MockClient {
|
|||
);
|
||||
|
||||
if (historyEntry) {
|
||||
this.doNotTouchWhileOffline =
|
||||
this.doNotTouchWhileOffline.filter(
|
||||
(file) => file !== historyEntry[1]
|
||||
utils.removeFromArray(
|
||||
this.doNotTouchWhileOffline,
|
||||
historyEntry[1]
|
||||
);
|
||||
}
|
||||
switch (logLine.level) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
# Parse arguments
|
||||
FIX_MODE=false
|
||||
if [[ "$1" == "--fix" ]]; then
|
||||
FIX_MODE=true
|
||||
|
|
@ -32,10 +31,29 @@ if [[ "$FIX_MODE" == true ]]; then
|
|||
else
|
||||
npm ci
|
||||
fi
|
||||
|
||||
|
||||
npm run build
|
||||
npm run test
|
||||
npm run lint
|
||||
|
||||
cd ../docs
|
||||
|
||||
if [[ "$FIX_MODE" == true ]]; then
|
||||
npm install
|
||||
npm run format
|
||||
npm run spell
|
||||
else
|
||||
npm ci
|
||||
npm run format:check
|
||||
npm run spell:check
|
||||
fi
|
||||
|
||||
cd ..
|
||||
# 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
|
||||
git status --porcelain
|
||||
echo "Failing CI because the working directory is not clean after linting"
|
||||
|
|
@ -44,8 +62,4 @@ fi
|
|||
|
||||
cd ..
|
||||
|
||||
if [[ "$FIX_MODE" == true ]]; then
|
||||
$0
|
||||
else
|
||||
echo "Success"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -109,4 +109,3 @@ while true; do
|
|||
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ cd -
|
|||
cp -r sync-server/bindings/* frontend/sync-client/src/services/types/
|
||||
|
||||
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 -
|
||||
|
|
|
|||
307
sync-server/Cargo.lock
generated
307
sync-server/Cargo.lock
generated
|
|
@ -17,6 +17,12 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
|
|
@ -114,7 +120,7 @@ checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -224,7 +230,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -256,7 +262,7 @@ dependencies = [
|
|||
"heck",
|
||||
"proc-macro-error",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"ubyte",
|
||||
]
|
||||
|
||||
|
|
@ -341,6 +347,8 @@ version = "1.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
|
|
@ -375,16 +383,6 @@ dependencies = [
|
|||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap-verbosity-flag"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
|
|
@ -406,7 +404,7 @@ dependencies = [
|
|||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -442,6 +440,15 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.16"
|
||||
|
|
@ -466,6 +473,15 @@ version = "2.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
|
|
@ -512,7 +528,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -523,9 +539,15 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
|||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dary_heap"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.6.0"
|
||||
|
|
@ -563,7 +585,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -657,6 +679,12 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
|
|
@ -733,7 +761,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -813,7 +841,18 @@ checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
|||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -822,7 +861,7 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1123,7 +1162,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1153,6 +1192,43 @@ dependencies = [
|
|||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01b7cb6ca682a621e7cda1c358c9724b53a7b4409be9be1dd443b7f3a26f998"
|
||||
dependencies = [
|
||||
"include-flate-codegen",
|
||||
"include-flate-compress",
|
||||
"libflate",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate-codegen"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f49bf5274aebe468d6e6eba14a977eaf1efa481dc173f361020de70c1c48050"
|
||||
dependencies = [
|
||||
"include-flate-compress",
|
||||
"libflate",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate-compress"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eae6a40e716bcd5931f5dbb79cd921512a4f647e2e9413fded3171fca3824dbc"
|
||||
dependencies = [
|
||||
"libflate",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.7.0"
|
||||
|
|
@ -1160,7 +1236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1175,6 +1251,16 @@ version = "1.0.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.76"
|
||||
|
|
@ -1200,6 +1286,30 @@ version = "0.2.174"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"core2",
|
||||
"crc32fast",
|
||||
"dary_heap",
|
||||
"libflate_lz77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libflate_lz77"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"hashbrown 0.16.1",
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.11"
|
||||
|
|
@ -1282,6 +1392,16 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
|
|
@ -1508,18 +1628,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
|
@ -1638,6 +1758,12 @@ version = "0.8.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.7"
|
||||
|
|
@ -1658,6 +1784,41 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
|
||||
dependencies = [
|
||||
"include-flate",
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.111",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
|
|
@ -1689,6 +1850,15 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.6.0"
|
||||
|
|
@ -1731,7 +1901,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1915,7 +2085,7 @@ dependencies = [
|
|||
"futures-intrusive",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"log",
|
||||
|
|
@ -1944,7 +2114,7 @@ dependencies = [
|
|||
"quote",
|
||||
"sqlx-core",
|
||||
"sqlx-macros-core",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1967,7 +2137,7 @@ dependencies = [
|
|||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
|
@ -2122,9 +2292,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -2143,13 +2313,14 @@ dependencies = [
|
|||
"bimap",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"futures",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"rand 0.9.0",
|
||||
"reconcile-text",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"sanitize-filename",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -2178,7 +2349,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2229,7 +2400,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2240,7 +2411,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2303,7 +2474,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2395,7 +2566,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2458,7 +2629,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
|
|
@ -2492,6 +2663,12 @@ version = "0.10.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.17"
|
||||
|
|
@ -2588,6 +2765,16 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
|
@ -2630,7 +2817,7 @@ dependencies = [
|
|||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
|
@ -2652,7 +2839,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
|
@ -2901,7 +3088,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
|
@ -2932,7 +3119,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2943,7 +3130,7 @@ checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2963,7 +3150,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
|
@ -2992,5 +3179,33 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -30,11 +30,12 @@ clap = { version = "4.5.38", features = ["derive"] }
|
|||
futures = "0.3.31"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_json = "1.0.140"
|
||||
clap-verbosity-flag = "3.0.3"
|
||||
bimap = "0.6.3"
|
||||
ts-rs = { version = "10.1", features = ["uuid-impl", "chrono-impl"] }
|
||||
base64 = "0.22.1"
|
||||
reconcile-text = { version = "0.8.0", features = ["serde"] }
|
||||
rust-embed = { version = "8.5.0", features = ["compression"] }
|
||||
mime_guess = "2.0.5"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,77 @@
|
|||
// generated by `sqlx migrate build-script`
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
println!("cargo:rerun-if-changed=../docs");
|
||||
|
||||
let docs_dir = PathBuf::from("../docs");
|
||||
let dist_dir = docs_dir.join(".vitepress").join("dist");
|
||||
|
||||
// Check if npm is available
|
||||
let npm_check = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", "npm", "--version"])
|
||||
.output()
|
||||
} else {
|
||||
Command::new("npm").arg("--version").output()
|
||||
};
|
||||
|
||||
match npm_check {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("cargo:warning=Building VitePress documentation...");
|
||||
|
||||
// Install dependencies if node_modules doesn't exist
|
||||
if !docs_dir.join("node_modules").exists() {
|
||||
println!("cargo:warning=Installing npm dependencies...");
|
||||
let npm_install = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", "npm", "install"])
|
||||
.current_dir(&docs_dir)
|
||||
.status()
|
||||
} else {
|
||||
Command::new("npm")
|
||||
.arg("install")
|
||||
.current_dir(&docs_dir)
|
||||
.status()
|
||||
};
|
||||
|
||||
if let Err(e) = npm_install {
|
||||
println!("cargo:warning=Failed to install npm dependencies: {e}");
|
||||
println!("cargo:warning=Docs will not be embedded in the binary");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the VitePress docs
|
||||
let build_result = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", "npm", "run", "build"])
|
||||
.current_dir(&docs_dir)
|
||||
.status()
|
||||
} else {
|
||||
Command::new("npm")
|
||||
.args(["run", "build"])
|
||||
.current_dir(&docs_dir)
|
||||
.status()
|
||||
};
|
||||
|
||||
match build_result {
|
||||
Ok(status) if status.success() => {
|
||||
println!("cargo:warning=VitePress docs built successfully at {dist_dir:?}");
|
||||
}
|
||||
Ok(status) => {
|
||||
println!("cargo:warning=VitePress build failed with status: {status}");
|
||||
println!("cargo:warning=Docs will not be embedded in the binary");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo:warning=Failed to build VitePress docs: {e}");
|
||||
println!("cargo:warning=Docs will not be embedded in the binary");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("cargo:warning=npm not found - skipping VitePress build");
|
||||
println!("cargo:warning=Docs will not be embedded in the binary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,3 +30,4 @@ users:
|
|||
logging:
|
||||
log_directory: logs
|
||||
log_rotation: 7days
|
||||
log_level: info
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ impl Database {
|
|||
|
||||
let mut connection_pools = std::collections::HashMap::new();
|
||||
|
||||
info!("Applying pending database migrations");
|
||||
let mut entries = tokio::fs::read_dir(&config.databases_directory_path).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if !entry.file_name().to_string_lossy().ends_with(".sqlite") {
|
||||
|
|
@ -319,7 +320,7 @@ impl Database {
|
|||
device_id,
|
||||
has_been_merged
|
||||
from latest_document_versions
|
||||
where relative_path = ?
|
||||
where relative_path = ? and is_deleted = false
|
||||
order by vault_update_id desc -- `latest_document_versions` only contains a single latest version of each document, however,
|
||||
-- multiple documents can have the same `relative_path`, if they have been deleted. That's
|
||||
-- why we only care about the latest version of the document with the given relative path.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::ffi::OsString;
|
||||
|
||||
use clap::Parser;
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
|
||||
use crate::cli::color_when::ColorWhen;
|
||||
|
||||
|
|
@ -12,9 +11,6 @@ pub struct Args {
|
|||
#[arg(index = 1)]
|
||||
pub config_path: Option<OsString>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub verbose: Verbosity<InfoLevel>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "WHEN",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ use std::time::Duration;
|
|||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::consts::{DEFAULT_LOG_DIRECTORY, DEFAULT_LOG_ROTATION_INTERVAL};
|
||||
use crate::{
|
||||
consts::{DEFAULT_LOG_DIRECTORY, DEFAULT_LOG_LEVEL, DEFAULT_LOG_ROTATION_INTERVAL},
|
||||
utils::log_level::LogLevel,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
|
|
@ -12,6 +15,9 @@ pub struct LoggingConfig {
|
|||
|
||||
#[serde(default = "default_log_rotation", with = "humantime_serde")]
|
||||
pub log_rotation: Duration,
|
||||
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: LogLevel,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
|
|
@ -19,6 +25,7 @@ impl Default for LoggingConfig {
|
|||
Self {
|
||||
log_directory: default_log_directory(),
|
||||
log_rotation: default_log_rotation(),
|
||||
log_level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,3 +39,8 @@ fn default_log_rotation() -> Duration {
|
|||
debug!("Using default log rotation: {DEFAULT_LOG_ROTATION_INTERVAL:?}");
|
||||
DEFAULT_LOG_ROTATION_INTERVAL
|
||||
}
|
||||
|
||||
fn default_log_level() -> LogLevel {
|
||||
debug!("Using default log level: Info");
|
||||
DEFAULT_LOG_LEVEL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::utils::log_level::LogLevel;
|
||||
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "config.yml";
|
||||
|
||||
pub const DEFAULT_DATABASES_DIRECTORY_PATH: &str = "databases";
|
||||
|
|
@ -14,6 +16,7 @@ pub const DEFAULT_MAX_CLIENTS_PER_VAULT: usize = 256;
|
|||
|
||||
pub const DEFAULT_LOG_DIRECTORY: &str = "logs";
|
||||
pub const DEFAULT_LOG_ROTATION_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); // 1 day
|
||||
pub const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info;
|
||||
|
||||
pub const DEFAULT_MERGEABLE_FILE_EXTENSIONS: &[&str] = &["md", "txt"];
|
||||
|
||||
|
|
|
|||
|
|
@ -60,14 +60,7 @@ fn set_up_logging(
|
|||
args: &Args,
|
||||
logging_config: &config::logging_config::LoggingConfig,
|
||||
) -> Result<(), SyncServerError> {
|
||||
let level_filter = match args.verbose.log_level_filter() {
|
||||
// We don't want to allow disabling all logging
|
||||
log::LevelFilter::Off | log::LevelFilter::Error => tracing::Level::ERROR,
|
||||
log::LevelFilter::Warn => tracing::Level::WARN,
|
||||
log::LevelFilter::Info => tracing::Level::INFO,
|
||||
log::LevelFilter::Debug => tracing::Level::DEBUG,
|
||||
log::LevelFilter::Trace => tracing::Level::TRACE,
|
||||
};
|
||||
let level_filter = logging_config.log_level.as_tracing_level();
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(level_filter.into())
|
||||
|
|
@ -77,7 +70,7 @@ fn set_up_logging(
|
|||
|
||||
let use_colors = args.color.use_colors();
|
||||
|
||||
let is_debug_mode = args.verbose.log_level_filter() >= log::LevelFilter::Debug;
|
||||
let is_debug_mode = logging_config.log_level.is_debug_or_trace();
|
||||
|
||||
let file_appender = RotatingFileWriter::new(
|
||||
&logging_config.log_directory,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ mod fetch_document_version;
|
|||
mod fetch_document_version_content;
|
||||
mod fetch_latest_document_version;
|
||||
mod fetch_latest_documents;
|
||||
mod index;
|
||||
mod ping;
|
||||
mod requests;
|
||||
mod responses;
|
||||
mod static_files;
|
||||
mod update_document;
|
||||
mod websocket;
|
||||
|
||||
|
|
@ -53,9 +53,10 @@ pub async fn create_server(config: Config) -> Result<()> {
|
|||
|
||||
let app = Router::new()
|
||||
.nest("/", get_authed_routes(app_state.clone()))
|
||||
.route("/", get(index::index))
|
||||
.route("/", get(static_files::serve_index))
|
||||
.route("/vaults/:vault_id/ping", get(ping::ping))
|
||||
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler))
|
||||
.route("/*path", get(static_files::serve_static_file))
|
||||
.layer(DefaultBodyLimit::disable())
|
||||
.layer(RequestBodyLimitLayer::new(
|
||||
app_state.config.server.max_body_size_mb * 1024 * 1024,
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
use axum::response::{Html, IntoResponse};
|
||||
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
const HTML_CONTENT: &str = include_str!("./assets/index.html");
|
||||
let html_content = HTML_CONTENT;
|
||||
Html(html_content)
|
||||
}
|
||||
55
sync-server/src/server/static_files.rs
Normal file
55
sync-server/src/server/static_files.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use axum::{
|
||||
extract::Path,
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use rust_embed::Embed;
|
||||
|
||||
#[derive(Embed)]
|
||||
#[folder = "../docs/.vitepress/dist"]
|
||||
pub struct DocsAssets;
|
||||
|
||||
pub async fn serve_static_file(Path(path): Path<String>) -> Response {
|
||||
let path = if path.is_empty() { "index.html" } else { &path };
|
||||
|
||||
match DocsAssets::get(path) {
|
||||
Some(content) => {
|
||||
let mime_type = mime_guess::from_path(path).first_or_octet_stream();
|
||||
let body = content.data.into_owned();
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime_type.as_ref())
|
||||
.header(header::CACHE_CONTROL, "public, max-age=3600")
|
||||
.body(body.into())
|
||||
.unwrap()
|
||||
}
|
||||
None => {
|
||||
// For SPA routing, if file not found, try serving index.html
|
||||
match DocsAssets::get("index.html") {
|
||||
Some(content) => {
|
||||
Html(String::from_utf8_lossy(&content.data).to_string()).into_response()
|
||||
}
|
||||
None => (StatusCode::NOT_FOUND, "Documentation not found").into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_index() -> Response {
|
||||
match DocsAssets::get("index.html") {
|
||||
Some(content) => Html(String::from_utf8_lossy(&content.data).to_string())
|
||||
.into_response(),
|
||||
None => {
|
||||
Html(r"
|
||||
<html>
|
||||
<head><title>VaultLink Server</title></head>
|
||||
<body>
|
||||
<h1>VaultLink Sync Server</h1>
|
||||
<p>Documentation not available. The server was compiled without embedded docs.</p>
|
||||
</body>
|
||||
</html>
|
||||
").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ pub mod dedup_paths;
|
|||
pub mod find_first_available_path;
|
||||
pub mod is_binary;
|
||||
pub mod is_file_type_mergable;
|
||||
pub mod log_level;
|
||||
pub mod normalize;
|
||||
pub mod rotating_file_writer;
|
||||
pub mod sanitize_path;
|
||||
|
|
|
|||
27
sync-server/src/utils/log_level.rs
Normal file
27
sync-server/src/utils/log_level.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
pub fn as_tracing_level(self) -> tracing::Level {
|
||||
match self {
|
||||
Self::Error => tracing::Level::ERROR,
|
||||
Self::Warn => tracing::Level::WARN,
|
||||
Self::Info => tracing::Level::INFO,
|
||||
Self::Debug => tracing::Level::DEBUG,
|
||||
Self::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_debug_or_trace(self) -> bool {
|
||||
matches!(self, Self::Debug | Self::Trace)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,6 +173,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_write_creates_log_file_and_directory() {
|
||||
let temp_dir = std::env::temp_dir().join("test_write_creates_log_file_and_directory");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
let mut writer =
|
||||
RotatingFileWriter::new(&temp_dir, "test", Duration::from_secs(3600)).unwrap();
|
||||
|
|
@ -195,6 +196,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_rotation_after_duration() {
|
||||
let temp_dir = std::env::temp_dir().join("test_rotation_after_duration");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
// Use a very short rotation duration
|
||||
// Note: We need to wait at least 1 second between rotations since
|
||||
|
|
@ -227,6 +229,7 @@ mod tests {
|
|||
fn test_calculate_next_rotation_time_no_existing_logs() {
|
||||
let temp_dir =
|
||||
std::env::temp_dir().join("test_calculate_next_rotation_time_no_existing_logs");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
fs::create_dir_all(&temp_dir).unwrap();
|
||||
|
||||
|
|
@ -248,6 +251,7 @@ mod tests {
|
|||
fn test_calculate_next_rotation_time_with_existing_log() {
|
||||
let temp_dir =
|
||||
std::env::temp_dir().join("test_calculate_next_rotation_time_with_existing_log");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
fs::create_dir_all(&temp_dir).unwrap();
|
||||
|
||||
|
|
@ -286,6 +290,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_picks_latest_log_file() {
|
||||
let temp_dir = std::env::temp_dir().join("test_picks_latest_log_file");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
fs::create_dir_all(&temp_dir).unwrap();
|
||||
|
||||
|
|
@ -320,6 +325,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_ignores_malformed_filenames() {
|
||||
let temp_dir = std::env::temp_dir().join("test_ignores_malformed_filenames");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
fs::create_dir_all(&temp_dir).unwrap();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue