Format without eclint
This commit is contained in:
parent
7438108885
commit
d13abc115d
35 changed files with 3273 additions and 3298 deletions
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
|
|
@ -5,8 +5,8 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
- "docs/**"
|
||||
- ".github/workflows/deploy-docs.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
|
|||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
|
|
|||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -5,6 +5,6 @@
|
|||
"**/dist": true,
|
||||
"**/node_modules": true,
|
||||
"**/.sqlx": true,
|
||||
"**/target": true,
|
||||
},
|
||||
"**/target": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -24,6 +24,7 @@ VaultLink is a self-hosted Obsidian plugin for real-time collaborative file sync
|
|||
## Development Commands
|
||||
|
||||
### Server Development
|
||||
|
||||
```bash
|
||||
cd sync-server
|
||||
cargo run config-e2e.yml # Start development server
|
||||
|
|
@ -36,6 +37,7 @@ cargo machete --with-metadata # Detect unused dependencies
|
|||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # Start development mode (watches sync-client and obsidian-plugin)
|
||||
|
|
@ -45,6 +47,7 @@ npm run lint # Lint and format TypeScript code
|
|||
```
|
||||
|
||||
### Database Setup (Development)
|
||||
|
||||
```bash
|
||||
cd sync-server
|
||||
sqlx database create --database-url sqlite://db.sqlite3
|
||||
|
|
@ -53,12 +56,14 @@ cargo sqlx prepare --workspace
|
|||
```
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Install required cargo tools
|
||||
cargo install sqlx-cli cargo-machete cargo-edit
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
- `scripts/check.sh`: Full CI check (builds, lints, tests both server and frontend)
|
||||
- `scripts/check.sh --fix`: Same as above but auto-fixes linting and formatting issues
|
||||
- `scripts/e2e.sh`: End-to-end testing
|
||||
|
|
@ -69,16 +74,20 @@ cargo install sqlx-cli cargo-machete cargo-edit
|
|||
## Code Structure
|
||||
|
||||
### Workspace Configuration
|
||||
|
||||
The frontend uses npm workspaces with four packages:
|
||||
|
||||
- `sync-client`: Core synchronization logic
|
||||
- `obsidian-plugin`: Obsidian-specific integration
|
||||
- `test-client`: Testing utilities
|
||||
- `local-client-cli`: Standalone CLI for VaultLink sync client
|
||||
|
||||
### Type Generation
|
||||
|
||||
Rust structs generate TypeScript types via ts-rs crate, stored in `sync-server/bindings/` and used by frontend packages.
|
||||
|
||||
### Key Files
|
||||
|
||||
- `sync-server/src/`: Rust server implementation with WebSocket handlers
|
||||
- `frontend/sync-client/src/sync-client.ts`: Main sync client entry point
|
||||
- `frontend/obsidian-plugin/src/vault-link-plugin.ts`: Main Obsidian plugin class
|
||||
|
|
@ -87,11 +96,13 @@ Rust structs generate TypeScript types via ts-rs crate, stored in `sync-server/b
|
|||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Server: `cargo test --verbose`
|
||||
- Frontend: `npm run test` (runs Jest across all workspaces)
|
||||
- E2E: `scripts/e2e.sh`
|
||||
|
||||
### Test Structure
|
||||
|
||||
- Rust: Unit tests alongside source files
|
||||
- TypeScript: `.test.ts` files using Jest
|
||||
- E2E: Uses test-client to simulate multiple concurrent users
|
||||
|
|
@ -99,12 +110,14 @@ Rust structs generate TypeScript types via ts-rs crate, stored in `sync-server/b
|
|||
## Code Style
|
||||
|
||||
### Rust
|
||||
|
||||
- Uses extensive Clippy lints (see Cargo.toml)
|
||||
- Follows pedantic linting rules
|
||||
- Forbids unsafe code
|
||||
- Uses cargo fmt with default settings
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Prettier configuration: 4-space tabs, trailing commas removed, LF line endings
|
||||
- ESLint with unused imports plugin
|
||||
- Consistent across all three frontend packages
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
## Develop
|
||||
|
||||
### Install [nvm](https://github.com/nvm-sh/nvm)
|
||||
### Set up Node.JS 25 with [nvm](https://github.com/nvm-sh/nvm)
|
||||
|
||||
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash`
|
||||
- `nvm install 22`
|
||||
- `nvm use 22`
|
||||
- Optionally set the system-wide default: `nvm alias default 22`
|
||||
- `nvm install 25`
|
||||
- `nvm use 25`
|
||||
- Optionally, set the system-wide default: `nvm alias default 25`
|
||||
|
||||
### Set up Rust
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@
|
|||
"version": "0.2",
|
||||
"language": "en-GB",
|
||||
"dictionaries": ["en-gb"],
|
||||
"ignorePaths": [
|
||||
"node_modules",
|
||||
".vitepress/dist",
|
||||
".vitepress/cache",
|
||||
"package-lock.json"
|
||||
],
|
||||
"ignorePaths": ["node_modules", ".vitepress/dist", ".vitepress/cache", "package-lock.json"],
|
||||
"words": [
|
||||
"VaultLink",
|
||||
"Obsidian",
|
||||
|
|
|
|||
|
|
@ -361,11 +361,11 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "upload_file",
|
||||
"path": "notes/example.md",
|
||||
"content": "File content here...",
|
||||
"base_version": 10,
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
"type": "upload_file",
|
||||
"path": "notes/example.md",
|
||||
"content": "File content here...",
|
||||
"base_version": 10,
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -373,8 +373,8 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "download_file",
|
||||
"path": "notes/example.md"
|
||||
"type": "download_file",
|
||||
"path": "notes/example.md"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -382,8 +382,8 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "delete_file",
|
||||
"path": "notes/old.md"
|
||||
"type": "delete_file",
|
||||
"path": "notes/old.md"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -391,8 +391,8 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "list_files",
|
||||
"since_version": 0
|
||||
"type": "list_files",
|
||||
"since_version": 0
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -402,11 +402,11 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "file_updated",
|
||||
"path": "notes/example.md",
|
||||
"version": 11,
|
||||
"size": 1024,
|
||||
"hash": "abc123..."
|
||||
"type": "file_updated",
|
||||
"path": "notes/example.md",
|
||||
"version": 11,
|
||||
"size": 1024,
|
||||
"hash": "abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -414,10 +414,10 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "file_content",
|
||||
"path": "notes/example.md",
|
||||
"content": "Updated content...",
|
||||
"version": 11
|
||||
"type": "file_content",
|
||||
"path": "notes/example.md",
|
||||
"content": "Updated content...",
|
||||
"version": 11
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -425,9 +425,9 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "file_deleted",
|
||||
"path": "notes/old.md",
|
||||
"version": 12
|
||||
"type": "file_deleted",
|
||||
"path": "notes/old.md",
|
||||
"version": 12
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -435,9 +435,9 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "sync_complete",
|
||||
"total_files": 150,
|
||||
"current_version": 200
|
||||
"type": "sync_complete",
|
||||
"total_files": 150,
|
||||
"current_version": 200
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -445,9 +445,9 @@ VALUES (?, ?, ?);
|
|||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"message": "File too large",
|
||||
"code": "FILE_TOO_LARGE"
|
||||
"type": "error",
|
||||
"message": "File too large",
|
||||
"code": "FILE_TOO_LARGE"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -243,9 +243,9 @@ users:
|
|||
2. Client sends authentication message:
|
||||
```json
|
||||
{
|
||||
"type": "auth",
|
||||
"token": "user-token",
|
||||
"vault": "vault-name"
|
||||
"type": "auth",
|
||||
"token": "user-token",
|
||||
"vault": "vault-name"
|
||||
}
|
||||
```
|
||||
3. Server validates:
|
||||
|
|
|
|||
5960
docs/package-lock.json
generated
5960
docs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -47,24 +47,24 @@ vaultlink \
|
|||
|
||||
### Required
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-l, --local-path <path>` | Local directory to sync |
|
||||
| `-r, --remote-uri <uri>` | Remote server WebSocket URI (ws:// or wss://) |
|
||||
| `-t, --token <token>` | Authentication token |
|
||||
| `-v, --vault-name <name>` | Vault name on server |
|
||||
| Option | Description |
|
||||
| ------------------------- | --------------------------------------------- |
|
||||
| `-l, --local-path <path>` | Local directory to sync |
|
||||
| `-r, --remote-uri <uri>` | Remote server WebSocket URI (ws:// or wss://) |
|
||||
| `-t, --token <token>` | Authentication token |
|
||||
| `-v, --vault-name <name>` | Vault name on server |
|
||||
|
||||
### Optional
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--sync-concurrency <number>` | `1` | Concurrent sync operations |
|
||||
| `--max-file-size-mb <number>` | `10` | Maximum file size in MB |
|
||||
| `--ignore-pattern <pattern>` | - | Glob pattern to ignore (repeatable) |
|
||||
| `--websocket-retry-interval-ms <ms>` | `3500` | WebSocket reconnection interval |
|
||||
| `--log-level <level>` | `INFO` | Log level: DEBUG, INFO, WARNING, ERROR |
|
||||
| `-h, --help` | - | Show help |
|
||||
| `-V, --version` | - | Show version |
|
||||
| Option | Default | Description |
|
||||
| ------------------------------------ | ------- | -------------------------------------- |
|
||||
| `--sync-concurrency <number>` | `1` | Concurrent sync operations |
|
||||
| `--max-file-size-mb <number>` | `10` | Maximum file size in MB |
|
||||
| `--ignore-pattern <pattern>` | - | Glob pattern to ignore (repeatable) |
|
||||
| `--websocket-retry-interval-ms <ms>` | `3500` | WebSocket reconnection interval |
|
||||
| `--log-level <level>` | `INFO` | Log level: DEBUG, INFO, WARNING, ERROR |
|
||||
| `-h, --help` | - | Show help |
|
||||
| `-V, --version` | - | Show version |
|
||||
|
||||
### Auto-Ignored Patterns
|
||||
|
||||
|
|
@ -74,11 +74,13 @@ vaultlink \
|
|||
### Examples
|
||||
|
||||
Basic usage:
|
||||
|
||||
```bash
|
||||
vaultlink -l ./vault -r wss://sync.example.com -t token123 -v default
|
||||
```
|
||||
|
||||
With ignore patterns:
|
||||
|
||||
```bash
|
||||
vaultlink -l ./vault -r wss://sync.example.com -t token123 -v default \
|
||||
--ignore-pattern "*.tmp" \
|
||||
|
|
@ -87,6 +89,7 @@ vaultlink -l ./vault -r wss://sync.example.com -t token123 -v default \
|
|||
```
|
||||
|
||||
With debug logging:
|
||||
|
||||
```bash
|
||||
vaultlink -l ./vault -r wss://sync.example.com -t token123 -v default \
|
||||
--log-level DEBUG
|
||||
|
|
@ -176,6 +179,7 @@ services:
|
|||
## Development
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# or from the parent folder, run
|
||||
|
|
@ -183,11 +187,13 @@ docker build -f local-client-cli/Dockerfile .
|
|||
```
|
||||
|
||||
Test:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
docker build -f local-client-cli/Dockerfile -t vault-link-cli:test .
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ export class FileWatcher {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a native platform path to forward slashes
|
||||
*/
|
||||
* Convert a native platform path to forward slashes
|
||||
*/
|
||||
private toUnixPath(nativePath: string): string {
|
||||
if (path.sep === "\\") {
|
||||
return nativePath.replace(/\\/g, "/");
|
||||
|
|
|
|||
|
|
@ -185,8 +185,8 @@ export class NodeFileSystemOperations implements FileSystemOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a forward-slash path to native platform path separators
|
||||
*/
|
||||
* Convert a forward-slash path to native platform path separators
|
||||
*/
|
||||
private toNativePath(relativePath: string): string {
|
||||
if (path.sep === "\\") {
|
||||
return relativePath.replace(/\//g, "\\");
|
||||
|
|
@ -195,8 +195,8 @@ export class NodeFileSystemOperations implements FileSystemOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a native platform path to forward slashes
|
||||
*/
|
||||
* Convert a native platform path to forward slashes
|
||||
*/
|
||||
private toUnixPath(nativePath: string): string {
|
||||
if (path.sep === "\\") {
|
||||
return nativePath.replace(/\\/g, "/");
|
||||
|
|
|
|||
|
|
@ -18,7 +18,5 @@
|
|||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist"
|
||||
]
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definiti
|
|||
**Note:** The Obsidian API is still in early alpha and is subject to change at any time!
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
|
|
@ -57,31 +58,6 @@ Quick starting guide for new plugin devs:
|
|||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
|
|
|
|||
|
|
@ -135,9 +135,9 @@ export default class VaultLinkPlugin extends Plugin {
|
|||
nativeLineEndings: Platform.isWin ? "\r\n" : "\n",
|
||||
...(IS_DEBUG_BUILD
|
||||
? {
|
||||
fetch: debugging.slowFetchFactory(1),
|
||||
webSocket: debugging.slowWebSocketFactory(1, new Logger())
|
||||
}
|
||||
fetch: debugging.slowFetchFactory(1),
|
||||
webSocket: debugging.slowWebSocketFactory(1, new Logger())
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -266,9 +266,8 @@ export class SyncSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Notice("Checking connection to the server...");
|
||||
new Notice(
|
||||
(
|
||||
await this.syncClient.checkConnection()
|
||||
).serverMessage
|
||||
(await this.syncClient.checkConnection())
|
||||
.serverMessage
|
||||
);
|
||||
await this.statusDescription.updateConnectionState();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@
|
|||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2024"
|
||||
]
|
||||
"lib": ["DOM", "ES2024"]
|
||||
},
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
"exclude": ["./dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ module.exports = (env, argv) => ({
|
|||
const source = path.resolve(__dirname, "dist");
|
||||
const destinations = [
|
||||
"/volumes/syncthing/Desktop/test/test/.obsidian/plugins/vault-link",
|
||||
"/volumes/syncthing/Desktop/test/test2/.obsidian/plugins/vault-link",
|
||||
"/volumes/syncthing/Desktop/test/test2/.obsidian/plugins/vault-link"
|
||||
// "/home/andras/obsidian-test/.obsidian/plugins/vault-link"
|
||||
];
|
||||
destinations.forEach((destination) => {
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ export class FileOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a file at the specified path.
|
||||
*
|
||||
* If a file with the same name already exists, it is moved before creating the new one.
|
||||
* Parent directories are created if necessary.
|
||||
*/
|
||||
* Create a file at the specified path.
|
||||
*
|
||||
* If a file with the same name already exists, it is moved before creating the new one.
|
||||
* Parent directories are created if necessary.
|
||||
*/
|
||||
public async create(
|
||||
path: RelativePath,
|
||||
newContent: Uint8Array
|
||||
|
|
@ -77,11 +77,11 @@ export class FileOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the file at the given path.
|
||||
*
|
||||
* Performs a 3-way merge before writing if the file's content differs from `expectedContent`.
|
||||
* Does not recreate the file if it no longer exists, returning an empty array instead.
|
||||
*/
|
||||
* Update the file at the given path.
|
||||
*
|
||||
* Performs a 3-way merge before writing if the file's content differs from `expectedContent`.
|
||||
* Does not recreate the file if it no longer exists, returning an empty array instead.
|
||||
*/
|
||||
public async write(
|
||||
path: RelativePath,
|
||||
expectedContent: Uint8Array,
|
||||
|
|
@ -239,12 +239,12 @@ export class FileOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Deconflicts the given path by appending (1), (2), etc. before the file extension until a non-existent path is found.
|
||||
* The returned path has a lock acquired on it; it must be released by the caller when no longer needed.
|
||||
*
|
||||
* @param path The starting path to deconflict
|
||||
* @returns a non-existent path with a lock acquired on it
|
||||
*/
|
||||
* Deconflicts the given path by appending (1), (2), etc. before the file extension until a non-existent path is found.
|
||||
* The returned path has a lock acquired on it; it must be released by the caller when no longer needed.
|
||||
*
|
||||
* @param path The starting path to deconflict
|
||||
* @returns a non-existent path with a lock acquired on it
|
||||
*/
|
||||
private async deconflictPath(path: RelativePath): Promise<RelativePath> {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [directory, fileName] = FileOperations.getParentDirAndFile(path);
|
||||
|
|
|
|||
|
|
@ -135,10 +135,10 @@ export class SafeFileSystemOperations implements FileSystemOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* Decorate an operation to ensure that the file exists before running it.
|
||||
* If the operation fails, it will check if the file still exists and throw
|
||||
* a FileNotFoundError if it doesn't.
|
||||
*/
|
||||
* Decorate an operation to ensure that the file exists before running it.
|
||||
* If the operation fails, it will check if the file still exists and throw
|
||||
* a FileNotFoundError if it doesn't.
|
||||
*/
|
||||
private async safeOperation<T>(
|
||||
path: RelativePath,
|
||||
operation: () => Promise<T>,
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class Database {
|
|||
i === 0
|
||||
? false
|
||||
: records[i - 1].parallelVersion ===
|
||||
current.parallelVersion
|
||||
current.parallelVersion
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -25,18 +25,18 @@ export class FetchController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether the fetch implementation can immediately send requests once outside of a reset.
|
||||
*/
|
||||
* Whether the fetch implementation can immediately send requests once outside of a reset.
|
||||
*/
|
||||
public get canFetch(): boolean {
|
||||
return this._canFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow or disallow fetching. The changes only take effect if not resetting.
|
||||
* When called during a reset, its effect is deferred until the reset is finished.
|
||||
*
|
||||
* @param canFetch Whether fetching is enabled
|
||||
*/
|
||||
* Allow or disallow fetching. The changes only take effect if not resetting.
|
||||
* When called during a reset, its effect is deferred until the reset is finished.
|
||||
*
|
||||
* @param canFetch Whether fetching is enabled
|
||||
*/
|
||||
public set canFetch(canFetch: boolean) {
|
||||
this._canFetch = canFetch;
|
||||
|
||||
|
|
@ -59,9 +59,9 @@ export class FetchController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Starts a reset, causing all ongoing and future fetches to be rejected
|
||||
* with a SyncResetError until finishReset is called.
|
||||
*/
|
||||
* Starts a reset, causing all ongoing and future fetches to be rejected
|
||||
* with a SyncResetError until finishReset is called.
|
||||
*/
|
||||
public startReset(): void {
|
||||
this.isResetting = true;
|
||||
this.rejectUntil(new SyncResetError());
|
||||
|
|
@ -72,9 +72,9 @@ export class FetchController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Finishes a reset, allowing fetches to proceed or wait again depending on
|
||||
* the current sync settings.
|
||||
*/
|
||||
* Finishes a reset, allowing fetches to proceed or wait again depending on
|
||||
* the current sync settings.
|
||||
*/
|
||||
public finishReset(): void {
|
||||
if (!this.isResetting) {
|
||||
return;
|
||||
|
|
@ -85,19 +85,19 @@ export class FetchController {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* |------------------|---------------|-----------------------------------------------------|
|
||||
* | | Sync enabled | Sync disabled |
|
||||
* |------------------|-------------- |-----------------------------------------------------|
|
||||
* | During reset | Rejects with SyncResetError without sending request |
|
||||
* |------------------|-------------- |-----------------------------------------------------|
|
||||
* | Outside of reset | Same as fetch | Blocks until sync is enabled and then same as fetch |
|
||||
* |------------------|---------------|-----------------------------------------------------|
|
||||
*
|
||||
* @param logger for errors
|
||||
* @param fetch to wrap
|
||||
* @returns a wrapped fetch implementation affected by the FetchController state
|
||||
*/
|
||||
*
|
||||
* |------------------|---------------|-----------------------------------------------------|
|
||||
* | | Sync enabled | Sync disabled |
|
||||
* |------------------|-------------- |-----------------------------------------------------|
|
||||
* | During reset | Rejects with SyncResetError without sending request |
|
||||
* |------------------|-------------- |-----------------------------------------------------|
|
||||
* | Outside of reset | Same as fetch | Blocks until sync is enabled and then same as fetch |
|
||||
* |------------------|---------------|-----------------------------------------------------|
|
||||
*
|
||||
* @param logger for errors
|
||||
* @param fetch to wrap
|
||||
* @returns a wrapped fetch implementation affected by the FetchController state
|
||||
*/
|
||||
public getControlledFetchImplementation(
|
||||
logger: Logger,
|
||||
fetch: typeof globalThis.fetch = globalThis.fetch
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
export interface CreateDocumentVersion {
|
||||
/**
|
||||
* The client can decide the document id (if it wishes to) in order
|
||||
* to help with syncing. If the client does not provide a document id,
|
||||
* the server will generate one. If the client provides a document id
|
||||
* it must not already exist in the database.
|
||||
*/
|
||||
* The client can decide the document id (if it wishes to) in order
|
||||
* to help with syncing. If the client does not provide a document id,
|
||||
* the server will generate one. If the client provides a document id
|
||||
* it must not already exist in the database.
|
||||
*/
|
||||
document_id: string | null;
|
||||
relative_path: string;
|
||||
content: number[];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont
|
|||
export interface FetchLatestDocumentsResponse {
|
||||
latestDocuments: DocumentVersionWithoutContent[];
|
||||
/**
|
||||
* The update ID of the latest document in the response.
|
||||
*/
|
||||
* The update ID of the latest document in the response.
|
||||
*/
|
||||
lastUpdateId: bigint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@
|
|||
*/
|
||||
export interface PingResponse {
|
||||
/**
|
||||
* Semantic version of the server.
|
||||
*/
|
||||
* Semantic version of the server.
|
||||
*/
|
||||
serverVersion: string;
|
||||
/**
|
||||
* Whether the client is authenticated based on the sent Authorization
|
||||
* header.
|
||||
*/
|
||||
* Whether the client is authenticated based on the sent Authorization
|
||||
* header.
|
||||
*/
|
||||
isAuthenticated: boolean;
|
||||
/**
|
||||
* List of file extensions that are allowed to be merged.
|
||||
*/
|
||||
* List of file extensions that are allowed to be merged.
|
||||
*/
|
||||
mergeableFileExtensions: string[];
|
||||
/**
|
||||
* API version ensuring backwards & forwards compatibility between the client
|
||||
* and server.
|
||||
*/
|
||||
* API version ensuring backwards & forwards compatibility between the client
|
||||
* and server.
|
||||
*/
|
||||
supportedApiVersion: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,10 +285,10 @@ export class SyncClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reload settings from disk overriding current in-memory settings.
|
||||
* Missing values will be filled in from DEFAULT_SETTINGS rather than
|
||||
* retaining current in-memory settings.
|
||||
*/
|
||||
* Reload settings from disk overriding current in-memory settings.
|
||||
* Missing values will be filled in from DEFAULT_SETTINGS rather than
|
||||
* retaining current in-memory settings.
|
||||
*/
|
||||
public async reloadSettings(): Promise<void> {
|
||||
this.checkIfDestroyed("reloadSettings");
|
||||
|
||||
|
|
@ -320,10 +320,10 @@ export class SyncClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wait for the in-flight operations to finish, reset all tracking,
|
||||
* and the local database but retain the settings.
|
||||
* The SyncClient can be used again after calling this method.
|
||||
*/
|
||||
* Wait for the in-flight operations to finish, reset all tracking,
|
||||
* and the local database but retain the settings.
|
||||
* The SyncClient can be used again after calling this method.
|
||||
*/
|
||||
public async reset(): Promise<void> {
|
||||
this.checkIfDestroyed("reset");
|
||||
|
||||
|
|
@ -436,9 +436,9 @@ export class SyncClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Completely destroy the SyncClient, cancelling all in-progress operations.
|
||||
* After calling this method, the SyncClient cannot be used again.
|
||||
*/
|
||||
* Completely destroy the SyncClient, cancelling all in-progress operations.
|
||||
* After calling this method, the SyncClient cannot be used again.
|
||||
*/
|
||||
public async destroy(): Promise<void> {
|
||||
this.checkIfDestroyed("destroy");
|
||||
|
||||
|
|
|
|||
|
|
@ -484,10 +484,10 @@ export class Syncer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create fake documents in the database for all files that are present locally
|
||||
* and also exist remotely. This will stop the subequent syncs from duplicating
|
||||
* the documents by creating the same documents from multiple clients.
|
||||
*/
|
||||
* Create fake documents in the database for all files that are present locally
|
||||
* and also exist remotely. This will stop the subequent syncs from duplicating
|
||||
* the documents by creating the same documents from multiple clients.
|
||||
*/
|
||||
private async createFakeDocumentsFromRemoteState(): Promise<void> {
|
||||
if (this.database.getHasInitialSyncCompleted()) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -170,14 +170,14 @@ export class UnrestrictedSyncer {
|
|||
const updateDetails: SyncUpdateDetails | SyncMovedDetails =
|
||||
oldPath !== undefined
|
||||
? {
|
||||
type: SyncType.MOVE,
|
||||
relativePath: document.relativePath,
|
||||
movedFrom: oldPath
|
||||
}
|
||||
type: SyncType.MOVE,
|
||||
relativePath: document.relativePath,
|
||||
movedFrom: oldPath
|
||||
}
|
||||
: {
|
||||
type: SyncType.UPDATE,
|
||||
relativePath: document.relativePath
|
||||
};
|
||||
type: SyncType.UPDATE,
|
||||
relativePath: document.relativePath
|
||||
};
|
||||
|
||||
await this.executeSync(updateDetails, async () => {
|
||||
const originalRelativePath = document.relativePath;
|
||||
|
|
@ -216,22 +216,22 @@ export class UnrestrictedSyncer {
|
|||
response =
|
||||
isText && cachedVersion !== undefined
|
||||
? await this.syncService.putText({
|
||||
documentId: document.documentId,
|
||||
parentVersionId:
|
||||
document.metadata.parentVersionId,
|
||||
relativePath: document.relativePath,
|
||||
content: diff(
|
||||
new TextDecoder().decode(cachedVersion),
|
||||
new TextDecoder().decode(contentBytes)
|
||||
)
|
||||
})
|
||||
documentId: document.documentId,
|
||||
parentVersionId:
|
||||
document.metadata.parentVersionId,
|
||||
relativePath: document.relativePath,
|
||||
content: diff(
|
||||
new TextDecoder().decode(cachedVersion),
|
||||
new TextDecoder().decode(contentBytes)
|
||||
)
|
||||
})
|
||||
: await this.syncService.putBinary({
|
||||
documentId: document.documentId,
|
||||
parentVersionId:
|
||||
document.metadata.parentVersionId,
|
||||
relativePath: document.relativePath,
|
||||
contentBytes
|
||||
});
|
||||
documentId: document.documentId,
|
||||
parentVersionId:
|
||||
document.metadata.parentVersionId,
|
||||
relativePath: document.relativePath,
|
||||
contentBytes
|
||||
});
|
||||
} else {
|
||||
if (!force) {
|
||||
this.logger.debug(
|
||||
|
|
@ -336,14 +336,14 @@ export class UnrestrictedSyncer {
|
|||
oldPath !== undefined ||
|
||||
response.relativePath != originalRelativePath
|
||||
? {
|
||||
type: SyncType.MOVE,
|
||||
relativePath: response.relativePath,
|
||||
movedFrom: originalRelativePath
|
||||
}
|
||||
type: SyncType.MOVE,
|
||||
relativePath: response.relativePath,
|
||||
movedFrom: originalRelativePath
|
||||
}
|
||||
: {
|
||||
type: SyncType.UPDATE,
|
||||
relativePath: response.relativePath
|
||||
};
|
||||
type: SyncType.UPDATE,
|
||||
relativePath: response.relativePath
|
||||
};
|
||||
|
||||
if (areThereLocalChanges) {
|
||||
this.history.addHistoryEntry({
|
||||
|
|
|
|||
|
|
@ -88,11 +88,11 @@ export class SyncHistory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Insert the entry at the beginning of the history list. If the entry
|
||||
* already in the list, it will get moved to the beginning and updated.
|
||||
*
|
||||
* If the entry list is too long, the oldest entry will be removed.
|
||||
*/
|
||||
* Insert the entry at the beginning of the history list. If the entry
|
||||
* already in the list, it will get moved to the beginning and updated.
|
||||
*
|
||||
* If the entry list is too long, the oldest entry will be removed.
|
||||
*/
|
||||
public addHistoryEntry(entry: CommonHistoryEntry): void {
|
||||
const historyEntry = {
|
||||
...entry,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ export function createClientId(): string {
|
|||
typeof navigator !== "undefined"
|
||||
? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated
|
||||
: typeof process !== "undefined"
|
||||
? process.platform
|
||||
: "unknown";
|
||||
? process.platform
|
||||
: "unknown";
|
||||
|
||||
return `vault-link/${packageVersion} (${uuidv4()}; ${platform})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,32 +13,32 @@ export class EventListeners<TListener extends (...args: any[]) => any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener to the collection.
|
||||
*
|
||||
* @param listener The listener callback to add
|
||||
* @returns An unsubscribe function that removes this listener when called
|
||||
*/
|
||||
* 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
|
||||
*/
|
||||
* 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
|
||||
*/
|
||||
* 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);
|
||||
|
|
@ -46,12 +46,12 @@ export class EventListeners<TListener extends (...args: any[]) => any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -21,34 +21,34 @@ export class Locks<T> {
|
|||
public constructor(private readonly logger?: Logger) {}
|
||||
|
||||
/**
|
||||
* Executes a function while holding exclusive locks on one or more keys.
|
||||
*
|
||||
* This method ensures that the provided function runs with exclusive access to the
|
||||
* specified key(s). Multiple keys are sorted to prevent deadlocks when different
|
||||
* operations request the same keys in different orders.
|
||||
*
|
||||
* @template R The return type of the function to execute
|
||||
* @param keyOrKeys A single key or array of keys to lock during function execution
|
||||
* @param fn The function to execute while holding the lock(s). Can be sync or async.
|
||||
* @returns A Promise that resolves to the return value of the executed function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Lock a single key
|
||||
* const result = await locks.withLock('file1', () => {
|
||||
* // Critical section - only one operation can access 'file1' at a time
|
||||
* return processFile('file1');
|
||||
* });
|
||||
*
|
||||
* // Lock multiple keys (prevents deadlocks through consistent ordering)
|
||||
* await locks.withLock(['file1', 'file2'], async () => {
|
||||
* // Critical section - exclusive access to both files
|
||||
* await moveFile('file1', 'file2');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @throws Any error thrown by the provided function will be propagated after locks are released
|
||||
*/
|
||||
* Executes a function while holding exclusive locks on one or more keys.
|
||||
*
|
||||
* This method ensures that the provided function runs with exclusive access to the
|
||||
* specified key(s). Multiple keys are sorted to prevent deadlocks when different
|
||||
* operations request the same keys in different orders.
|
||||
*
|
||||
* @template R The return type of the function to execute
|
||||
* @param keyOrKeys A single key or array of keys to lock during function execution
|
||||
* @param fn The function to execute while holding the lock(s). Can be sync or async.
|
||||
* @returns A Promise that resolves to the return value of the executed function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Lock a single key
|
||||
* const result = await locks.withLock('file1', () => {
|
||||
* // Critical section - only one operation can access 'file1' at a time
|
||||
* return processFile('file1');
|
||||
* });
|
||||
*
|
||||
* // Lock multiple keys (prevents deadlocks through consistent ordering)
|
||||
* await locks.withLock(['file1', 'file2'], async () => {
|
||||
* // Critical section - exclusive access to both files
|
||||
* await moveFile('file1', 'file2');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @throws Any error thrown by the provided function will be propagated after locks are released
|
||||
*/
|
||||
public async withLock<R>(
|
||||
keyOrKeys: T | T[],
|
||||
fn: () => R | Promise<R>
|
||||
|
|
@ -83,12 +83,12 @@ export class Locks<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Attempts to acquire a lock immediately without waiting.
|
||||
* Must call `unlock()` if successful.
|
||||
*
|
||||
* @param key The key to lock
|
||||
* @returns `true` if lock acquired, `false` if already locked
|
||||
*/
|
||||
* Attempts to acquire a lock immediately without waiting.
|
||||
* Must call `unlock()` if successful.
|
||||
*
|
||||
* @param key The key to lock
|
||||
* @returns `true` if lock acquired, `false` if already locked
|
||||
*/
|
||||
public tryLock(key: T): boolean {
|
||||
if (this.locked.has(key)) {
|
||||
return false;
|
||||
|
|
@ -100,12 +100,12 @@ export class Locks<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Waits to acquire a lock, blocking until available.
|
||||
* Operations are queued in FIFO order. Must call `unlock()` when done.
|
||||
*
|
||||
* @param key The key to wait for and lock
|
||||
* @returns Promise that resolves when lock is acquired
|
||||
*/
|
||||
* Waits to acquire a lock, blocking until available.
|
||||
* Operations are queued in FIFO order. Must call `unlock()` when done.
|
||||
*
|
||||
* @param key The key to wait for and lock
|
||||
* @returns Promise that resolves when lock is acquired
|
||||
*/
|
||||
public async waitForLock(key: T): Promise<void> {
|
||||
if (this.tryLock(key)) {
|
||||
return Promise.resolve();
|
||||
|
|
@ -126,12 +126,12 @@ export class Locks<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Releases a lock and grants access to the next waiting operation in FIFO order.
|
||||
* Removes the key from locked set if no waiters.
|
||||
*
|
||||
* @param key The key to unlock
|
||||
* @throws {Error} If key is not currently locked
|
||||
*/
|
||||
* Releases a lock and grants access to the next waiting operation in FIFO order.
|
||||
* Removes the key from locked set if no waiters.
|
||||
*
|
||||
* @param key The key to unlock
|
||||
* @throws {Error} If key is not currently locked
|
||||
*/
|
||||
public unlock(key: T): void {
|
||||
if (!this.locked.has(key)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,5 @@
|
|||
"declaration": true,
|
||||
"declarationDir": "./dist/types"
|
||||
},
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
"exclude": ["./dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,8 @@
|
|||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2024",
|
||||
],
|
||||
"lib": ["DOM", "ES2024"],
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
"exclude": ["./dist"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,24 +9,24 @@ server:
|
|||
max_clients_per_vault: 256
|
||||
response_timeout: 30m
|
||||
mergeable_file_extensions:
|
||||
- md
|
||||
- txt
|
||||
- md
|
||||
- txt
|
||||
users:
|
||||
user_configs:
|
||||
- name: admin
|
||||
token: test-token-change-me
|
||||
vault_access:
|
||||
type: allow_access_to_all
|
||||
- name: other-admin
|
||||
token: test-token-change-me2
|
||||
vault_access:
|
||||
type: allow_access_to_all
|
||||
- name: test
|
||||
token: other-test-token
|
||||
vault_access:
|
||||
type: allow_list
|
||||
allowed:
|
||||
- default
|
||||
- name: admin
|
||||
token: test-token-change-me
|
||||
vault_access:
|
||||
type: allow_access_to_all
|
||||
- name: other-admin
|
||||
token: test-token-change-me2
|
||||
vault_access:
|
||||
type: allow_access_to_all
|
||||
- name: test
|
||||
token: other-test-token
|
||||
vault_access:
|
||||
type: allow_list
|
||||
allowed:
|
||||
- default
|
||||
logging:
|
||||
log_directory: logs
|
||||
log_rotation: 7days
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue