mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-17 11:50:16 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72033472f3 | |||
| 93dc03e86f | |||
| dae8443fe8 | |||
| 88a8bcbd5a | |||
| 4a5283543d | |||
| 7895336189 | |||
| 2d5cdccf7d | |||
| 497fd04081 | |||
| ae9c46f8f0 | |||
| dcd10cd690 | |||
| 5a35b71339 | |||
| 18a59219f7 | |||
| c9095738e6 | |||
| bed415fc6b | |||
| 4eeb93b9e4 | |||
| 0fc233686b | |||
| def1d297f5 | |||
| e7cf2a6fba | |||
| 2da2fd7671 | |||
| d5175969e7 | |||
| 46f9630999 | |||
| 75adcf0ff0 | |||
| 866dd49ba3 | |||
| d0a84d07aa | |||
| 089a4c0f8b | |||
| 1a1f816872 | |||
| 9d86c2828b | |||
| 3b6d3beaa7 | |||
| bb75b6ead8 | |||
| fccb2304f6 | |||
| f00ef5eaae | |||
| 4e7ee760de | |||
| c4faade30c | |||
| 295dc1392a | |||
| 445a8c747c | |||
| 292a6b9e1e | |||
| 0e04e7d31d | |||
| 4cf4acf7e9 | |||
| 0856693aac | |||
| 39d78a04ac | |||
| 0b8d73ccd8 | |||
| 5921a71227 | |||
| a40929c9e4 | |||
| 2d8a285201 | |||
| d9903bfe9e | |||
| a6e7dddf7f | |||
| 60f21eb9d2 | |||
| 6b7816d334 | |||
| 369e62ee8d | |||
| 37593bbee6 | |||
| baa51a66a7 | |||
| c78e583399 | |||
| 2f10121d6c | |||
| 3ab80190d6 | |||
| 8948bf2803 | |||
| 486fd15c60 | |||
| 5fd85c71ca | |||
| c1f41910c4 | |||
| 3693d6a6b6 | |||
| cc3c992b1d | |||
| df390ac456 |
@@ -1,17 +1,43 @@
|
|||||||
name: cli-deno-tests
|
name: cli-deno-tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- beta
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/cli-deno-tests.yml'
|
||||||
|
- 'src/apps/cli/**'
|
||||||
|
- 'src/lib/src/API/processSetting.ts'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/cli-deno-tests.yml'
|
||||||
|
- 'src/apps/cli/**'
|
||||||
|
- 'src/lib/src/API/processSetting.ts'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
test_task:
|
test_task:
|
||||||
description: 'Deno test task to run'
|
description: 'Deno test task to run'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- test
|
- test:ci
|
||||||
|
- test:p2p
|
||||||
|
- test:all
|
||||||
- test:local
|
- test:local
|
||||||
- test:e2e-matrix
|
- test:e2e-matrix
|
||||||
- test:p2p-sync
|
default: test:ci
|
||||||
default: test
|
enable_debug:
|
||||||
|
description: 'Enable verbose and debug logging'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
use_coturn:
|
||||||
|
description: 'Enable local coturn container for P2P tests'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -27,21 +53,24 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SELECTED_TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test' }}"
|
SELECTED_TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test:ci' }}"
|
||||||
echo "[INFO] Selected task set: $SELECTED_TASK"
|
echo "[INFO] Selected task set: $SELECTED_TASK"
|
||||||
|
|
||||||
case "$SELECTED_TASK" in
|
case "$SELECTED_TASK" in
|
||||||
test)
|
test:ci)
|
||||||
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:push-pull","test:sync-two-local","test:sync-locked-remote","test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download","test:e2e-couchdb","test:e2e-matrix"]'
|
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
|
||||||
|
;;
|
||||||
|
test:p2p)
|
||||||
|
TASK_MATRIX='["test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download"]'
|
||||||
|
;;
|
||||||
|
test:all)
|
||||||
|
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
|
||||||
;;
|
;;
|
||||||
test:local)
|
test:local)
|
||||||
TASK_MATRIX='["test:setup-put-cat","test:mirror"]'
|
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon"]'
|
||||||
;;
|
;;
|
||||||
test:e2e-matrix)
|
test:e2e-matrix)
|
||||||
TASK_MATRIX='["test:e2e-matrix"]'
|
TASK_MATRIX='["test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
|
||||||
;;
|
|
||||||
test:p2p-sync)
|
|
||||||
TASK_MATRIX='["test:p2p-sync"]'
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "[ERROR] Unknown task set: $SELECTED_TASK" >&2
|
echo "[ERROR] Unknown task set: $SELECTED_TASK" >&2
|
||||||
@@ -55,6 +84,8 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
DENO_DIR: ~/.cache/deno
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -70,12 +101,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version: '24.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v2
|
uses: denoland/setup-deno@v2
|
||||||
with:
|
with:
|
||||||
deno-version: v2.x
|
deno-version: v2.x
|
||||||
|
|
||||||
|
- name: Cache Deno dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/deno
|
||||||
|
key: ${{ runner.os }}-deno-${{ hashFiles('src/apps/cli/testdeno/deno.lock', 'src/apps/cli/testdeno/deno.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-deno-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -102,6 +142,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
LIVESYNC_DOCKER_MODE: native
|
LIVESYNC_DOCKER_MODE: native
|
||||||
LIVESYNC_CLI_RETRY: 3
|
LIVESYNC_CLI_RETRY: 3
|
||||||
|
LIVESYNC_CLI_DEBUG: ${{ inputs.enable_debug == true && '1' || '0' }}
|
||||||
|
LIVESYNC_CLI_VERBOSE: ${{ inputs.enable_debug == true && '1' || '0' }}
|
||||||
|
LIVESYNC_USE_COTURN: ${{ inputs.use_coturn == true && '1' || '0' }}
|
||||||
run: |
|
run: |
|
||||||
TASK="${{ matrix.task }}"
|
TASK="${{ matrix.task }}"
|
||||||
echo "[INFO] Running Deno task: $TASK"
|
echo "[INFO] Running Deno task: $TASK"
|
||||||
@@ -110,5 +153,5 @@ jobs:
|
|||||||
- name: Stop leftover containers
|
- name: Stop leftover containers
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker stop couchdb-test minio-test relay-test >/dev/null 2>&1 || true
|
docker stop couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
|
||||||
docker rm couchdb-test minio-test relay-test >/dev/null 2>&1 || true
|
docker rm couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
|
||||||
|
|||||||
@@ -8,8 +8,21 @@ name: Build and Push CLI Docker Image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- "*.*.*-cli"
|
- "*.*.*-cli"
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "*.md"
|
||||||
|
- "images/**"
|
||||||
|
- "assets/**"
|
||||||
|
- "instruction_images/**"
|
||||||
|
- "src/apps/webapp/**"
|
||||||
|
- "src/apps/webpeer/**"
|
||||||
|
- ".github/workflows/release.yml"
|
||||||
|
- ".github/workflows/unit-ci.yml"
|
||||||
|
- ".github/workflows/harness-ci.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
dry_run:
|
dry_run:
|
||||||
@@ -41,14 +54,32 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(jq -r '.version' manifest.json)
|
VERSION=$(jq -r '.version' manifest.json)
|
||||||
EPOCH=$(date +%s)
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
TAG="${VERSION}-${EPOCH}-cli"
|
|
||||||
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
|
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
|
||||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
|
# Build tag list based on the event and git ref
|
||||||
echo "full=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT
|
TAGS=""
|
||||||
echo "version=${IMAGE}:${VERSION}-cli" >> $GITHUB_OUTPUT
|
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
echo "latest=${IMAGE}:latest" >> $GITHUB_OUTPUT
|
# Stable release builds
|
||||||
|
TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli"
|
||||||
|
elif [[ "${{ github.ref }}" == refs/heads/main ]]; then
|
||||||
|
# Bleeding-edge / nightly builds
|
||||||
|
TAGS="${IMAGE}:edge,${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli"
|
||||||
|
else
|
||||||
|
# Other branches / manual run fallback
|
||||||
|
TAGS="${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine if the image should be pushed
|
||||||
|
PUSH="true"
|
||||||
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
|
if [[ "${{ inputs.dry_run }}" == "true" ]]; then
|
||||||
|
PUSH="false"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "push=${PUSH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -92,10 +123,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: src/apps/cli/Dockerfile
|
file: src/apps/cli/Dockerfile
|
||||||
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
|
push: ${{ steps.meta.outputs.push }}
|
||||||
tags: |
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
${{ steps.meta.outputs.full }}
|
|
||||||
${{ steps.meta.outputs.version }}
|
|
||||||
${{ steps.meta.outputs.latest }}
|
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
@@ -12,23 +12,6 @@ on:
|
|||||||
- two-vaults-couchdb
|
- two-vaults-couchdb
|
||||||
- two-vaults-minio
|
- two-vaults-minio
|
||||||
default: two-vaults-matrix
|
default: two-vaults-matrix
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- beta
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/cli-e2e.yml'
|
|
||||||
- 'src/apps/cli/**'
|
|
||||||
- 'src/lib/src/API/processSetting.ts'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/cli-e2e.yml'
|
|
||||||
- 'src/apps/cli/**'
|
|
||||||
- 'src/lib/src/API/processSetting.ts'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
# Sequence of patterns matched against refs/tags
|
# Sequence of patterns matched against refs/tags
|
||||||
tags:
|
tags:
|
||||||
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
||||||
|
- '!*-cli' # Exclude command-line interface tags
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
+111
-68
@@ -1,68 +1,111 @@
|
|||||||
# Run Unit test without Harnesses
|
# Run Unit test without Harnesses
|
||||||
name: unit-ci
|
name: unit-ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- beta
|
- beta
|
||||||
paths:
|
paths:
|
||||||
- 'src/**'
|
- 'src/**'
|
||||||
- 'test/**'
|
- 'test/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'package-lock.json'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
- 'vite.config.ts'
|
- 'vite.config.ts'
|
||||||
- 'vitest.config*.ts'
|
- 'vitest.config*.ts'
|
||||||
- 'esbuild.config.mjs'
|
- 'esbuild.config.mjs'
|
||||||
- 'eslint.config.mjs'
|
- 'eslint.config.mjs'
|
||||||
- '.github/workflows/unit-ci.yml'
|
- '.github/workflows/unit-ci.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'src/**'
|
- 'src/**'
|
||||||
- 'test/**'
|
- 'test/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'package-lock.json'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
- 'vite.config.ts'
|
- 'vite.config.ts'
|
||||||
- 'vitest.config*.ts'
|
- 'vitest.config*.ts'
|
||||||
- 'esbuild.config.mjs'
|
- 'esbuild.config.mjs'
|
||||||
- 'eslint.config.mjs'
|
- 'eslint.config.mjs'
|
||||||
- '.github/workflows/unit-ci.yml'
|
- '.github/workflows/unit-ci.yml'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
unit-test:
|
||||||
runs-on: ubuntu-latest
|
name: Unit Tests
|
||||||
timeout-minutes: 30
|
runs-on: ubuntu-latest
|
||||||
steps:
|
timeout-minutes: 30
|
||||||
- name: Checkout
|
steps:
|
||||||
uses: actions/checkout@v4
|
- name: Checkout
|
||||||
with:
|
uses: actions/checkout@v4
|
||||||
submodules: recursive
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
- name: Setup Node.js
|
||||||
with:
|
uses: actions/setup-node@v4
|
||||||
node-version: '24.x'
|
with:
|
||||||
cache: 'npm'
|
node-version: '24.x'
|
||||||
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
# unit tests do not require Playwright, so we can skip installing its dependencies to save time
|
|
||||||
# - name: Install test dependencies (Playwright Chromium)
|
- name: Run unit tests suite with coverage
|
||||||
# run: npm run test:install-dependencies
|
run: npm run test:unit:coverage
|
||||||
|
|
||||||
- name: Run unit tests suite with coverage
|
- name: Upload coverage report
|
||||||
run: npm run test:unit:coverage
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
- name: Upload coverage report
|
with:
|
||||||
if: always()
|
name: unit-coverage-report
|
||||||
uses: actions/upload-artifact@v4
|
path: coverage/**
|
||||||
with:
|
|
||||||
name: coverage-report
|
integration-test:
|
||||||
path: coverage/**
|
name: Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create environment configuration files
|
||||||
|
run: |
|
||||||
|
cat <<EOF > .env
|
||||||
|
BUILD_MODE=dev
|
||||||
|
PATHS_TEST_INSTALL=
|
||||||
|
EOF
|
||||||
|
cat <<EOF > .test.env
|
||||||
|
hostname=http://127.0.0.1:5989/
|
||||||
|
dbname=livesync-test-db2
|
||||||
|
username=admin
|
||||||
|
password=testpassword
|
||||||
|
minioEndpoint=http://127.0.0.1:9000
|
||||||
|
accessKey=minioadmin
|
||||||
|
secretKey=minioadmin
|
||||||
|
bucketName=livesync-test-bucket
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Start CouchDB container
|
||||||
|
run: npm run test:docker-couchdb:start
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: npm run test:integration
|
||||||
|
|
||||||
|
- name: Stop CouchDB container
|
||||||
|
if: always()
|
||||||
|
run: npm run test:docker-couchdb:stop || true
|
||||||
@@ -32,6 +32,9 @@ Always adhere to the following stylistic and spelling rules:
|
|||||||
- Use **'dialogue'** in documentation, user-facing messages, and general text. Use **'dialog'** only inside source code (e.g. class names, methods).
|
- Use **'dialogue'** in documentation, user-facing messages, and general text. Use **'dialog'** only inside source code (e.g. class names, methods).
|
||||||
- Use the hyphenated form **'plug-in'** in user-facing text. Use **'plugin'** only in codebase files, configuration settings, or technical contexts.
|
- Use the hyphenated form **'plug-in'** in user-facing text. Use **'plugin'** only in codebase files, configuration settings, or technical contexts.
|
||||||
|
|
||||||
|
5. **User Communication Language**:
|
||||||
|
- Always reply to the user in the language in which they asked the question.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technical & Architecture Rules
|
## Technical & Architecture Rules
|
||||||
|
|||||||
@@ -54,8 +54,13 @@ To facilitate development and testing, the build process can automatically copy
|
|||||||
|
|
||||||
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
|
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
|
||||||
- This is now obsolete, migrated to vitest.
|
- This is now obsolete, migrated to vitest.
|
||||||
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests.
|
- **Vitest**:
|
||||||
- Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`).
|
- **Unit Tests** (`vitest.config.unit.ts`): Unit tests run in Node.js (excluding harnesses and integration tests). Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). Executed via `npm run test:unit`.
|
||||||
|
- **Integration Tests** (`vitest.config.integration.ts`): Tests run in Node.js against a real CouchDB instance. Integration tests should be `*.integration.spec.ts` or `*.integration.test.ts` and placed alongside the implementation file (e.g., `StreamingFetch.integration.spec.ts`). Executed via `npm run test:integration`.
|
||||||
|
- If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server.
|
||||||
|
- **E2E Tests** (`vitest.config.ts`): End-to-end tests run in a browser-based harness using Playwright/Chromium to test full synchronisation scenarios. Executed via `npm run test`.
|
||||||
|
- **P2P Tests** (`vitest.config.p2p.ts`): Browser-based Peer-to-Peer replication tests. Executed via `npm run test:p2p`.
|
||||||
|
- **RPC Unit Tests** (`vitest.config.rpc-unit.ts`): RPC-specific unit tests with coverage thresholds.
|
||||||
|
|
||||||
- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services:
|
- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services:
|
||||||
```bash
|
```bash
|
||||||
@@ -63,11 +68,11 @@ To facilitate development and testing, the build process can automatically copy
|
|||||||
npm run test:full # Run tests with coverage
|
npm run test:full # Run tests with coverage
|
||||||
npm run test:docker-all:stop # Stop services
|
npm run test:docker-all:stop # Stop services
|
||||||
```
|
```
|
||||||
If some services are not needed, start only required ones (e.g., `test:docker-couchdb:start`)
|
If some services are not needed, start only required ones (e.g., `test:docker-couchdb:start`).
|
||||||
Note that if services are already running, starting script will fail. Please stop them first.
|
Note that if services are already running, starting script will fail. Please stop them first.
|
||||||
|
|
||||||
- **Test Structure**:
|
- **Test Structure**:
|
||||||
- `test/suite/` - Integration tests for sync operations
|
- `test/suite/` - E2E tests for sync operations (running in browser)
|
||||||
- `test/unit/` - Unit tests (via vitest, as harness is browser-based)
|
- `test/unit/` - Unit tests (via vitest, as harness is browser-based)
|
||||||
- `test/harness/` - Mock implementations (e.g., `obsidian-mock.ts`)
|
- `test/harness/` - Mock implementations (e.g., `obsidian-mock.ts`)
|
||||||
|
|
||||||
@@ -151,7 +156,7 @@ Hence, the new feature should be implemented as follows:
|
|||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
### Module Implementation
|
### Module Implementation (Now not recommended for new features, use services instead)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class ModuleExample extends AbstractObsidianModule {
|
export class ModuleExample extends AbstractObsidianModule {
|
||||||
@@ -204,6 +209,7 @@ In short, the situation remains unchanged for me, but it means you all become a
|
|||||||
## Contribution Guidelines
|
## Contribution Guidelines
|
||||||
|
|
||||||
- Follow existing code style and conventions
|
- Follow existing code style and conventions
|
||||||
|
- Write integration tests (`*.integration.spec.ts` or `*.integration.test.ts`) when adding or modifying features that interact with the remote database, and ensure that they pass in the CI workflow.
|
||||||
- Please bump dependencies with care, check artifacts after updates, with diff-tools and only expected changes in the build output (to avoid unexpected vulnerabilities).
|
- Please bump dependencies with care, check artifacts after updates, with diff-tools and only expected changes in the build output (to avoid unexpected vulnerabilities).
|
||||||
- When adding new features, please consider it has an OSS implementation, and avoid using proprietary services or APIs that may limit usage.
|
- When adding new features, please consider it has an OSS implementation, and avoid using proprietary services or APIs that may limit usage.
|
||||||
- For example, any functionality to connect to a new type of server is expected to either have an OSS implementation available for that server, or to be managed under some responsibilities and/or limitations without disrupting existing functionality, and scope for surveillance reduced by some means (e.g., by client-side encryption, auditing the server ourselves).
|
- For example, any functionality to connect to a new type of server is expected to either have an OSS implementation available for that server, or to be managed under some responsibilities and/or limitations without disrupting existing functionality, and scope for surveillance reduced by some means (e.g., by client-side encryption, auditing the server ourselves).
|
||||||
|
|||||||
@@ -488,6 +488,11 @@ Automatically Sync all files when opening Obsidian.
|
|||||||
Setting key: syncAfterMerge
|
Setting key: syncAfterMerge
|
||||||
Sync automatically after merging files
|
Sync automatically after merging files
|
||||||
|
|
||||||
|
#### Keep replication active in the background
|
||||||
|
|
||||||
|
Setting key: keepReplicationActiveInBackground
|
||||||
|
Desktop only; uses more battery and network.
|
||||||
|
|
||||||
### 3. Update thinning
|
### 3. Update thinning
|
||||||
|
|
||||||
#### Batch database update
|
#### Batch database update
|
||||||
|
|||||||
+35
-2
@@ -19,11 +19,15 @@ const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
|||||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||||
|
|
||||||
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
||||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length);
|
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter)
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => p.length);
|
||||||
if (PATH_TEST_INSTALL) {
|
if (PATH_TEST_INSTALL) {
|
||||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
console.log(
|
||||||
|
"Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleAliasPlugin = {
|
const moduleAliasPlugin = {
|
||||||
@@ -66,6 +70,34 @@ const moduleAliasPlugin = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removePragmaCommentsPlugin = {
|
||||||
|
name: "remove-pragma-comments",
|
||||||
|
setup(build) {
|
||||||
|
// Filter target extensions (e.g., JavaScript and TypeScript)
|
||||||
|
build.onLoad({ filter: /\.[jt]s?$/ }, async (args) => {
|
||||||
|
const source = await fs.promises.readFile(args.path, "utf8");
|
||||||
|
|
||||||
|
// Regex targeting both single-line and multi-line comments
|
||||||
|
// This regex looks for:
|
||||||
|
// - /* eslint ... */ (multi-line)
|
||||||
|
// const esLintPragmaRegexBlock = /\/\*[\s\S]*?eslint[\s\S]*?\*\/|([^\\:]|^)\/\/.*eslint.*$/gm;
|
||||||
|
// - // eslint-disable-next-line
|
||||||
|
let cleanedSource = source;
|
||||||
|
const tsIgnoreRegex = /\/\*\s*@ts-ignore\s*\*\/|([^\\:]|^)\/\/.*?@ts-ignore.*$/gm;
|
||||||
|
const esLintPragmaRegexLine = /([^\\:]|^)\/\/.*?eslint-.*$/gm;
|
||||||
|
const exps = [tsIgnoreRegex, esLintPragmaRegexLine];
|
||||||
|
for (const exp of exps) {
|
||||||
|
cleanedSource = cleanedSource.replace(exp, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: cleanedSource,
|
||||||
|
loader: args.path.endsWith("ts") ? "ts" : "js",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/** @type esbuild.Plugin[] */
|
/** @type esbuild.Plugin[] */
|
||||||
const plugins = [
|
const plugins = [
|
||||||
{
|
{
|
||||||
@@ -177,6 +209,7 @@ const context = await esbuild.context({
|
|||||||
preprocess: sveltePreprocess(),
|
preprocess: sveltePreprocess(),
|
||||||
compilerOptions: { css: "injected", preserveComments: false },
|
compilerOptions: { css: "injected", preserveComments: false },
|
||||||
}),
|
}),
|
||||||
|
removePragmaCommentsPlugin,
|
||||||
...plugins,
|
...plugins,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
const restrictedGlobalsOptions = [
|
||||||
|
{
|
||||||
|
name: "app",
|
||||||
|
message: "Avoid using the global app object. Instead use the reference provided by your plugin instance.",
|
||||||
|
},
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
name: "fetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `fetch` for network requests in Obsidian.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "localStorage",
|
||||||
|
message:
|
||||||
|
"Prefer `App#saveLocalStorage` / `App#loadLocalStorage` functions to write / read localStorage data that's unique to a vault.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const restrictedImportsOptions = [
|
||||||
|
{
|
||||||
|
name: "axios",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `axios`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "superagent",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `superagent`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "got",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `got`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ofetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `ofetch`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ky",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `ky`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node-fetch",
|
||||||
|
message: "Use the built-in `requestUrl` function instead of `node-fetch`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "moment",
|
||||||
|
message: "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const warnWhileDev = "off";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const baseRules = {
|
||||||
|
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-unused-labels": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"require-await": "off",
|
||||||
|
// -- TypeScript specific rules (Gradual adoption of stricter rules, currently set to 'warn' for a while).
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||||
|
// -- TypeScript specific rules
|
||||||
|
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
||||||
|
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
||||||
|
// Rules is now set to 'off' for a while.
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
// -- Reasonable rules.
|
||||||
|
"@typescript-eslint/no-deprecated": warnWhileDev,
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/require-await": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
|
||||||
|
// -- General rules
|
||||||
|
"no-async-promise-executor": warnWhileDev,
|
||||||
|
"no-constant-condition": ["error", { checkLoops: false }],
|
||||||
|
// -- Disabled rules
|
||||||
|
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
||||||
|
"no-undef": "off",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const obsidianRules = {
|
||||||
|
// -- Obsidian rules
|
||||||
|
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
|
||||||
|
"obsidianmd/no-unsupported-api": warnWhileDev,
|
||||||
|
|
||||||
|
// -- Plugin specific overrides
|
||||||
|
"obsidianmd/rule-custom-message": "off",
|
||||||
|
"obsidianmd/ui/sentence-case": "off",
|
||||||
|
"obsidianmd/no-plugin-as-component": "off",
|
||||||
|
|
||||||
|
// -- Temporary overrides for migration
|
||||||
|
"obsidianmd/no-static-styles-assignment": "off",
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @type {(base:string) => import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const ImportAliasRules = (base) => ({
|
||||||
|
"@dword-design/import-alias/prefer-alias": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
aliasForSubpaths: true,
|
||||||
|
alias: {
|
||||||
|
"@": `${base}/src`,
|
||||||
|
"@lib": `${base}/src/lib/src`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* @type {import("eslint").Linter.RulesRecord}
|
||||||
|
*/
|
||||||
|
export const CommunityReviewRecommendedRules = {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-prototype-bultins": "off",
|
||||||
|
"no-self-compare": "warn",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"no-implicit-globals": "error",
|
||||||
|
"no-console": "off", // overridden by obsidianmd/rule-custom-message
|
||||||
|
"no-restricted-globals": ["error", ...restrictedGlobalsOptions],
|
||||||
|
"no-restricted-imports": ["error", ...restrictedImportsOptions],
|
||||||
|
"no-alert": "error",
|
||||||
|
"no-undef": "error",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-deprecated": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
|
||||||
|
// "import/no-nodejs-modules": "off",
|
||||||
|
// "import/no-extraneous-dependencies": "error",
|
||||||
|
};
|
||||||
+17
-49
@@ -4,6 +4,8 @@ import globals from "globals";
|
|||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import * as sveltePlugin from "eslint-plugin-svelte";
|
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||||
import svelteParser from "svelte-eslint-parser";
|
import svelteParser from "svelte-eslint-parser";
|
||||||
|
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||||
|
import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs";
|
||||||
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
@@ -18,11 +20,11 @@ export default defineConfig([
|
|||||||
"**/*.json",
|
"**/*.json",
|
||||||
"**/.eslintrc.js.bak",
|
"**/.eslintrc.js.bak",
|
||||||
// Files from linked dependencies (those files should not exist for most people).
|
// Files from linked dependencies (those files should not exist for most people).
|
||||||
"modules/octagonal-wheels/dist/**/*",
|
"modules/octagonal-wheels/dist",
|
||||||
|
|
||||||
// Sub-projects (Exclude from root linting as they have different environments)
|
// Sub-projects (Exclude from root linting as they have different environments)
|
||||||
"src/apps/**/*",
|
"src/apps",
|
||||||
"utils/**/*",
|
"utils",
|
||||||
|
|
||||||
// Specific exclusions from common library (src/lib)
|
// Specific exclusions from common library (src/lib)
|
||||||
"src/lib/coverage",
|
"src/lib/coverage",
|
||||||
@@ -54,6 +56,7 @@ export default defineConfig([
|
|||||||
]),
|
]),
|
||||||
...sveltePlugin.configs["flat/base"],
|
...sveltePlugin.configs["flat/base"],
|
||||||
...obsidianmd.configs.recommended,
|
...obsidianmd.configs.recommended,
|
||||||
|
importAlias.configs.recommended,
|
||||||
{
|
{
|
||||||
files: ["**/*.ts"],
|
files: ["**/*.ts"],
|
||||||
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
|
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
|
||||||
@@ -62,64 +65,29 @@ export default defineConfig([
|
|||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "./",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
linterOptions:{
|
linterOptions: {
|
||||||
reportUnusedDisableDirectives: false,
|
reportUnusedDisableDirectives: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
...baseRules,
|
||||||
"no-unused-vars": "off",
|
...obsidianRules,
|
||||||
"no-unused-labels": "off",
|
// -- Project specific rules
|
||||||
"no-prototype-builtins": "off",
|
...ImportAliasRules("."),
|
||||||
"require-await": "off",
|
|
||||||
// -- TypeScript specific rules
|
|
||||||
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
|
||||||
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
|
||||||
// Rules is now set to 'off' for a while.
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
||||||
// -- Reasonable rules.
|
|
||||||
"@typescript-eslint/no-deprecated": warnWhileDev,
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"@typescript-eslint/require-await": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
|
||||||
|
|
||||||
// -- Obsidian rules
|
|
||||||
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
|
|
||||||
"obsidianmd/no-unsupported-api": warnWhileDev,
|
|
||||||
|
|
||||||
// -- General rules
|
|
||||||
"no-async-promise-executor": warnWhileDev,
|
|
||||||
"no-constant-condition": ["error", { checkLoops: false }],
|
|
||||||
// -- Disabled rules
|
|
||||||
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
|
||||||
"no-undef": "off",
|
|
||||||
|
|
||||||
// -- Plugin specific overrides
|
|
||||||
"obsidianmd/rule-custom-message": "off",
|
|
||||||
"obsidianmd/ui/sentence-case": "off",
|
|
||||||
"obsidianmd/no-plugin-as-component": "off",
|
|
||||||
|
|
||||||
// -- Temporary overrides for migration
|
|
||||||
"obsidianmd/no-static-styles-assignment": "off",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/*.svelte"],
|
files: ["**/*.svelte"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||||
parser: svelteParser,
|
parser: svelteParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
extraFileExtensions: [".svelte"],
|
extraFileExtensions: [".svelte"],
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
rootDir: "./",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
@@ -127,8 +95,8 @@ export default defineConfig([
|
|||||||
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
|
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
|
||||||
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"obsidianmd/no-plugin-as-component": "off",
|
...obsidianRules,
|
||||||
"obsidianmd/ui/sentence-case": "off",
|
...ImportAliasRules("."),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.74",
|
"version": "0.25.76",
|
||||||
"minAppVersion": "1.7.2",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
Generated
+1613
-2328
File diff suppressed because it is too large
Load Diff
+29
-24
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.74",
|
"version": "0.25.76",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,10 +25,12 @@
|
|||||||
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
||||||
"prettyCheck": "npm run prettyNoWrite -- --check",
|
"prettyCheck": "npm run prettyNoWrite -- --check",
|
||||||
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
||||||
"check": "npm run tsc-check && npm run lint && npm run svelte-check",
|
"check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15",
|
||||||
|
"check": "npm run tsc-check && npm run lint && npm run svelte-check && npm run check:compatibility",
|
||||||
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/",
|
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:unit": "vitest run --config vitest.config.unit.ts",
|
"test:unit": "vitest run --config vitest.config.unit.ts",
|
||||||
|
"test:integration": "vitest run --config vitest.config.integration.ts",
|
||||||
"test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage",
|
"test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage",
|
||||||
"test:install-playwright": "npx playwright install chromium",
|
"test:install-playwright": "npx playwright install chromium",
|
||||||
"test:install-dependencies": "npm run test:install-playwright",
|
"test:install-dependencies": "npm run test:install-playwright",
|
||||||
@@ -55,15 +57,17 @@
|
|||||||
"test:docker-all:stop": "npm run test:docker-all:down",
|
"test:docker-all:stop": "npm run test:docker-all:down",
|
||||||
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
|
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
|
||||||
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
|
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"update-workspaces": "node update-workspaces.mjs",
|
||||||
|
"version": "node version-bump.mjs && node update-workspaces.mjs && git add manifest.json versions.json src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||||
"@eslint/js": "^9.39.3",
|
"@eslint/js": "^9.39.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"@tsconfig/svelte": "^5.0.8",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
"@types/deno": "^2.5.0",
|
"@types/deno": "^2.5.0",
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
@@ -78,23 +82,21 @@
|
|||||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
"@types/pouchdb-replication": "^6.4.7",
|
"@types/pouchdb-replication": "^6.4.7",
|
||||||
"@types/transform-pouch": "^1.0.6",
|
"@types/transform-pouch": "^1.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
|
||||||
"@typescript-eslint/parser": "8.56.1",
|
"@typescript-eslint/parser": "8.56.1",
|
||||||
"@vitest/browser": "^4.1.8",
|
"@vitest/browser": "^4.1.8",
|
||||||
"@vitest/browser-playwright": "^4.1.8",
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
"@vitest/coverage-v8": "^4.1.8",
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.28.1",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"esbuild-svelte": "^0.9.4",
|
"esbuild-svelte": "^0.9.4",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.19.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"globals": "^14.0.0",
|
"globals": "^14.0.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-load-config": "^6.0.1",
|
|
||||||
"pouchdb-adapter-http": "^9.0.0",
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
"pouchdb-adapter-idb": "^9.0.0",
|
"pouchdb-adapter-idb": "^9.0.0",
|
||||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||||
@@ -108,20 +110,22 @@
|
|||||||
"pouchdb-utils": "^9.0.0",
|
"pouchdb-utils": "^9.0.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
"svelte": "5.41.1",
|
"svelte": "5.56.3",
|
||||||
"svelte-check": "^4.4.3",
|
"svelte-check": "^4.6.0",
|
||||||
|
"svelte-eslint-parser": "^1.8.0",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"transform-pouch": "^2.0.0",
|
"transform-pouch": "^2.0.0",
|
||||||
"tslib": "^2.8.1",
|
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1",
|
"typescript-eslint": "^8.61.0",
|
||||||
"vite-plugin-istanbul": "^8.0.0",
|
"vite": "^8.0.16",
|
||||||
"vitest": "^4.1.8",
|
"vitest": "^4.1.8",
|
||||||
"webdriverio": "^9.27.0",
|
"webdriverio": "^9.27.0",
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2",
|
||||||
|
"@emnapi/core": "1.11.1",
|
||||||
|
"@emnapi/runtime": "1.11.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.808.0",
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
@@ -130,21 +134,22 @@
|
|||||||
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||||
"@smithy/protocol-http": "^5.3.9",
|
"@smithy/protocol-http": "^5.3.9",
|
||||||
"@smithy/querystring-builder": "^4.2.9",
|
"@smithy/querystring-builder": "^4.2.9",
|
||||||
|
"@smithy/types": "^4.14.3",
|
||||||
"@smithy/util-retry": "^4.4.5",
|
"@smithy/util-retry": "^4.4.5",
|
||||||
"@trystero-p2p/nostr": "^0.24.0",
|
"@trystero-p2p/nostr": "^0.24.0",
|
||||||
"chokidar": "^4.0.0",
|
|
||||||
"commander": "^14.0.3",
|
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"markdown-it": "^14.1.1",
|
"markdown-it": "^14.2.0",
|
||||||
"micromatch": "^4.0.0",
|
"minimatch": "^10.2.5",
|
||||||
"minimatch": "^10.2.2",
|
"obsidian": "^1.13.1",
|
||||||
"obsidian": "^1.12.3",
|
|
||||||
"octagonal-wheels": "^0.1.46",
|
"octagonal-wheels": "^0.1.46",
|
||||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
|
||||||
"qrcode-generator": "^1.4.4",
|
"qrcode-generator": "^1.4.4",
|
||||||
"werift": "^0.23.0",
|
|
||||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||||
}
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"src/apps/cli",
|
||||||
|
"src/apps/webpeer",
|
||||||
|
"src/apps/webapp"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ test/test-init.local.sh
|
|||||||
node_modules
|
node_modules
|
||||||
.*.json
|
.*.json
|
||||||
*.env
|
*.env
|
||||||
!.test.env
|
!.test.env
|
||||||
|
bench-results
|
||||||
@@ -82,8 +82,8 @@ RUN apt-get update \
|
|||||||
|
|
||||||
WORKDIR /deps
|
WORKDIR /deps
|
||||||
|
|
||||||
# runtime-package.json lists only the packages that Vite leaves external
|
# package.json lists only the packages that the CLI requires
|
||||||
COPY src/apps/cli/runtime-package.json ./package.json
|
COPY src/apps/cli/package.json ./package.json
|
||||||
RUN npm install --omit=dev
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+12
-5
@@ -118,19 +118,26 @@ git submodule update --init --recursive
|
|||||||
# Install dependencies from the repository root
|
# Install dependencies from the repository root
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Build the CLI from its package directory
|
# Build the CLI from the repository root
|
||||||
|
npm run build -w self-hosted-livesync-cli
|
||||||
|
|
||||||
|
# Or from the package directory
|
||||||
cd src/apps/cli
|
cd src/apps/cli
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
If `src/lib` is missing, `npm run build` now stops early with a targeted message
|
If `src/lib` is missing, the build process stops early with a targeted message instead of a low-level Vite `ENOENT` error.
|
||||||
instead of a low-level Vite `ENOENT` error.
|
|
||||||
|
|
||||||
Run the CLI:
|
Run the CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run with npm script (from repository root)
|
# Run with npm workspace script (from repository root)
|
||||||
npm run --silent cli -- [database-path] [command] [args...]
|
npm run cli -w self-hosted-livesync-cli -- [database-path] [command] [args...]
|
||||||
|
|
||||||
|
# Or from the package directory
|
||||||
|
cd src/apps/cli
|
||||||
|
npm run cli -- [database-path] [command] [args...]
|
||||||
|
|
||||||
# Run the built executable directly
|
# Run the built executable directly
|
||||||
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
|
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from "path";
|
|
||||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||||
|
import { path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversion adapter implementation for Node.js
|
* Conversion adapter implementation for Node.js
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import type { FilePath, UXStat } from "@lib/common/types";
|
import type { FilePath, UXStat } from "@lib/common/types";
|
||||||
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
|
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
|
||||||
import { NodePathAdapter } from "./NodePathAdapter";
|
import { NodePathAdapter } from "./NodePathAdapter";
|
||||||
@@ -8,6 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
|
|||||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||||
|
import { fsPromises as fs, path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete file system adapter implementation for Node.js
|
* Complete file system adapter implementation for Node.js
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from "path";
|
|
||||||
import type { FilePath } from "@lib/common/types";
|
import type { FilePath } from "@lib/common/types";
|
||||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeFile } from "./NodeTypes";
|
import type { NodeFile } from "./NodeTypes";
|
||||||
|
import { path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path adapter implementation for Node.js
|
* Path adapter implementation for Node.js
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeStat } from "./NodeTypes";
|
import type { NodeStat } from "./NodeTypes";
|
||||||
|
import { fsPromises as fs, path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage adapter implementation for Node.js
|
* Storage adapter implementation for Node.js
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||||
|
import { fsPromises as fs, path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vault adapter implementation for Node.js
|
* Vault adapter implementation for Node.js
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
import { addP2PEventHandlers } from "@lib/replication/trystero/addP2PEventHandlers";
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
type CLIP2PPeer = {
|
type CLIP2PPeer = {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function delay(ms: number): Promise<void> {
|
function delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
|
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
|
||||||
import { configURIBase } from "@lib/common/models/shared.const";
|
import { configURIBase } from "@lib/common/models/shared.const";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +16,8 @@ import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelative
|
|||||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
||||||
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
|
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
|
||||||
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
import { fsPromises as fs, path } from "../node-compat";
|
||||||
|
|
||||||
function redactConnectionString(uri: string): string {
|
function redactConnectionString(uri: string): string {
|
||||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||||
@@ -150,11 +150,11 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollTimer = setTimeout(poll, currentIntervalMs);
|
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||||
};
|
};
|
||||||
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll, currentIntervalMs);
|
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||||
clearTimeout(pollTimer);
|
compatGlobal.clearTimeout(pollTimer);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as path from "path";
|
import { path, readline } from "../node-compat";
|
||||||
import * as readline from "node:readline/promises";
|
|
||||||
|
|
||||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
// eslint-disable -- This is the entry point for the CLI application.
|
||||||
import * as polyfill from "werift";
|
import * as polyfill from "werift";
|
||||||
import { main } from "./main";
|
import { main } from "./main";
|
||||||
|
|
||||||
|
|||||||
+13
-19
@@ -1,17 +1,10 @@
|
|||||||
/**
|
|
||||||
* Self-hosted LiveSync CLI
|
|
||||||
* Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
||||||
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
||||||
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
|
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
||||||
import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types";
|
import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
||||||
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
|
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
|
||||||
import {
|
import {
|
||||||
LOG_LEVEL_DEBUG,
|
LOG_LEVEL_DEBUG,
|
||||||
setGlobalLogFunction,
|
setGlobalLogFunction,
|
||||||
@@ -26,7 +19,8 @@ import type { CLICommand, CLIOptions } from "./commands/types";
|
|||||||
import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
|
import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
|
||||||
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
||||||
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
||||||
import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature";
|
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
|
||||||
|
import { fsPromises as fs, path, fs as fsSync } from "./node-compat";
|
||||||
|
|
||||||
const SETTINGS_FILE = ".livesync/settings.json";
|
const SETTINGS_FILE = ".livesync/settings.json";
|
||||||
ensureGlobalNodeLocalStorage();
|
ensureGlobalNodeLocalStorage();
|
||||||
@@ -238,8 +232,8 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
|||||||
const targetPath = options.settingsPath
|
const targetPath = options.settingsPath
|
||||||
? path.resolve(options.settingsPath)
|
? path.resolve(options.settingsPath)
|
||||||
: options.commandArgs[0]
|
: options.commandArgs[0]
|
||||||
? path.resolve(options.commandArgs[0])
|
? path.resolve(options.commandArgs[0])
|
||||||
: path.resolve(process.cwd(), "data.json");
|
: path.resolve(process.cwd(), "data.json");
|
||||||
|
|
||||||
if (!options.force) {
|
if (!options.force) {
|
||||||
try {
|
try {
|
||||||
@@ -329,8 +323,8 @@ export async function main() {
|
|||||||
options.command === "mirror" && options.commandArgs[0]
|
options.command === "mirror" && options.commandArgs[0]
|
||||||
? path.resolve(options.commandArgs[0])
|
? path.resolve(options.commandArgs[0])
|
||||||
: options.vaultPath
|
: options.vaultPath
|
||||||
? path.resolve(options.vaultPath)
|
? path.resolve(options.vaultPath)
|
||||||
: databasePath!;
|
: databasePath!;
|
||||||
|
|
||||||
// Check if vault directory exists
|
// Check if vault directory exists
|
||||||
try {
|
try {
|
||||||
@@ -485,8 +479,8 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||||
|
|
||||||
// Save the settings file before any lifecycle events can mutate and persist them.
|
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||||
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||||
@@ -499,8 +493,8 @@ export async function main() {
|
|||||||
if (settingsBackup) {
|
if (settingsBackup) {
|
||||||
const tmpPath = settingsPath + ".tmp";
|
const tmpPath = settingsPath + ".tmp";
|
||||||
try {
|
try {
|
||||||
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
|
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||||
require("fs").renameSync(tmpPath, settingsPath);
|
fsSync.renameSync(tmpPath, settingsPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Settings] Failed to restore settings on exit:", err);
|
console.error("[Settings] Failed to restore settings on exit:", err);
|
||||||
}
|
}
|
||||||
@@ -563,7 +557,7 @@ export async function main() {
|
|||||||
|
|
||||||
if (options.command === "daemon" && result) {
|
if (options.command === "daemon" && result) {
|
||||||
// Keep the process running
|
// Keep the process running
|
||||||
await new Promise(() => {});
|
await new Promise(() => { });
|
||||||
} else {
|
} else {
|
||||||
await core.services.control.onUnload();
|
await core.services.control.onUnload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import type {
|
|||||||
IStorageEventWatchHandlers,
|
IStorageEventWatchHandlers,
|
||||||
} from "@lib/managers/adapters";
|
} from "@lib/managers/adapters";
|
||||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||||
import type { NodeFile, NodeFolder } from "../adapters/NodeTypes";
|
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
|
||||||
import type { Stats } from "fs";
|
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
||||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||||
|
import { fsPromises as fs, path, type Stats } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific type guard adapter
|
* CLI-specific type guard adapter
|
||||||
@@ -101,7 +99,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
private basePath: string,
|
private basePath: string,
|
||||||
private ignoreRules?: IgnoreRules,
|
private ignoreRules?: IgnoreRules,
|
||||||
private watchEnabled: boolean = false
|
private watchEnabled: boolean = false
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
|
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
|
||||||
import type { NodeFile } from "../adapters/NodeTypes";
|
import type { NodeFile } from "@/apps/cli/adapters/NodeTypes";
|
||||||
|
|
||||||
// ── chokidar mock ──────────────────────────────────────────────────────────────
|
// ── chokidar mock ──────────────────────────────────────────────────────────────
|
||||||
// Must be hoisted before imports that pull in chokidar.
|
// Must be hoisted before imports that pull in chokidar.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
||||||
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
||||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||||
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
|
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
|
||||||
|
|
||||||
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {
|
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint-disable obsidianmd/no-nodejs-builtins */
|
||||||
|
import * as nodeFs from "node:fs";
|
||||||
|
import * as nodeFsPromises from "node:fs/promises";
|
||||||
|
import * as nodePath from "node:path";
|
||||||
|
import * as nodeReadlinePromises from "node:readline/promises";
|
||||||
|
import type { Stats } from "node:fs";
|
||||||
|
export {
|
||||||
|
nodeFs as fs,
|
||||||
|
nodeFsPromises as fsPromises,
|
||||||
|
nodePath as path,
|
||||||
|
nodeReadlinePromises as readline,
|
||||||
|
type Stats,
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.25.76-cli",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -38,6 +38,26 @@
|
|||||||
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
||||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands"
|
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
"devDependencies": {}
|
"chokidar": "^4.0.0",
|
||||||
|
"minimatch": "^10.2.5",
|
||||||
|
"octagonal-wheels": "^0.1.46",
|
||||||
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-errors": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
|
"pouchdb-merge": "^9.0.0",
|
||||||
|
"pouchdb-replication": "^9.0.0",
|
||||||
|
"pouchdb-utils": "^9.0.0",
|
||||||
|
"transform-pouch": "^2.0.0",
|
||||||
|
"werift": "^0.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "livesync-cli-runtime",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "Runtime dependencies for Self-hosted LiveSync CLI Docker image",
|
|
||||||
"dependencies": {
|
|
||||||
"chokidar": "^4.0.0",
|
|
||||||
"commander": "^14.0.3",
|
|
||||||
"werift": "^0.22.9",
|
|
||||||
"pouchdb-adapter-http": "^9.0.0",
|
|
||||||
"pouchdb-adapter-idb": "^9.0.0",
|
|
||||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
|
||||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
|
||||||
"pouchdb-adapter-memory": "^9.0.0",
|
|
||||||
"pouchdb-core": "^9.0.0",
|
|
||||||
"pouchdb-errors": "^9.0.0",
|
|
||||||
"pouchdb-find": "^9.0.0",
|
|
||||||
"pouchdb-mapreduce": "^9.0.0",
|
|
||||||
"pouchdb-merge": "^9.0.0",
|
|
||||||
"pouchdb-replication": "^9.0.0",
|
|
||||||
"pouchdb-utils": "^9.0.0",
|
|
||||||
"pouchdb-wrappers": "*",
|
|
||||||
"transform-pouch": "^2.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
||||||
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
|
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
|
||||||
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
|
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||||
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
|
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
|
||||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import { FileAccessCLI } from "./FileAccessCLI";
|
import { FileAccessCLI } from "./FileAccessCLI";
|
||||||
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
|
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
|
||||||
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
||||||
import { StorageEventManagerCLI } from "../managers/StorageEventManagerCLI";
|
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI";
|
||||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||||
import type { IgnoreRules } from "./IgnoreRules";
|
import type { IgnoreRules } from "./IgnoreRules";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific implementation of FileAccessBase
|
* CLI-specific implementation of FileAccessBase
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import * as fs from "fs/promises";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import { minimatch } from "minimatch";
|
import { minimatch } from "minimatch";
|
||||||
|
import { fsPromises as fs, path } from "../node-compat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific implementation of ServiceFileAccess
|
* CLI-specific implementation of ServiceFileAccess
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
|
|||||||
import type { IVaultService } from "@lib/services/base/IService";
|
import type { IVaultService } from "@lib/services/base/IService";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||||
import * as nodeFs from "node:fs";
|
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||||
import * as nodePath from "node:path";
|
|
||||||
|
|
||||||
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
||||||
const NODE_KV_VALUES_KEY = "values";
|
const NODE_KV_VALUES_KEY = "values";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as nodeFs from "node:fs";
|
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||||
import * as nodePath from "node:path";
|
|
||||||
|
|
||||||
type LocalStorageShape = {
|
type LocalStorageShape = {
|
||||||
getItem(key: string): string | null;
|
getItem(key: string): string | null;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
|
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
|
||||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import * as nodePath from "node:path";
|
|
||||||
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
|
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
|
||||||
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
|
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
|
||||||
import { UIService } from "@lib/services/implements/base/UIService";
|
import { UIService } from "@lib/services/implements/base/UIService";
|
||||||
@@ -24,7 +23,8 @@ import type { ServiceInstances } from "@lib/services/ServiceHub";
|
|||||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||||
import { NodeSettingService } from "./NodeSettingService";
|
import { NodeSettingService } from "./NodeSettingService";
|
||||||
import { DatabaseService } from "@lib/services/base/DatabaseService";
|
import { DatabaseService } from "@lib/services/base/DatabaseService";
|
||||||
import type { ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||||
|
import { path as nodePath } from "../node-compat";
|
||||||
|
|
||||||
export class NodeServiceContext extends ServiceContext {
|
export class NodeServiceContext extends ServiceContext {
|
||||||
databasePath: string;
|
databasePath: string;
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ cli_test_init_settings_file "$SETTINGS_FILE"
|
|||||||
# isConfigured=true is required for mirror (canProceedScan checks this)
|
# isConfigured=true is required for mirror (canProceedScan checks this)
|
||||||
cli_test_mark_settings_configured "$SETTINGS_FILE"
|
cli_test_mark_settings_configured "$SETTINGS_FILE"
|
||||||
|
|
||||||
|
# Enable writeDocumentsIfConflicted to resolve unsynced conflicts during mirror
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const file = process.argv[1];
|
||||||
|
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||||
|
data.writeDocumentsIfConflicted = true;
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
' "$SETTINGS_FILE"
|
||||||
|
|
||||||
# Preparation: Sync settings and files logic
|
# Preparation: Sync settings and files logic
|
||||||
DB_SETTINGS="$DB_DIR/settings.json"
|
DB_SETTINGS="$DB_DIR/settings.json"
|
||||||
cp "$SETTINGS_FILE" "$DB_SETTINGS"
|
cp "$SETTINGS_FILE" "$DB_SETTINGS"
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.ts";
|
||||||
|
import { assertFilesEqual, runCliOrFail } from "./helpers/cli.ts";
|
||||||
|
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
|
||||||
|
import { createDeterministicDataset, type DatasetEntry } from "./helpers/dataset.ts";
|
||||||
|
|
||||||
|
type BenchmarkConfig = {
|
||||||
|
couchdbBackendUri: string;
|
||||||
|
couchdbProxyUri: string;
|
||||||
|
couchdbUser: string;
|
||||||
|
couchdbPassword: string;
|
||||||
|
couchdbDbname: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
datasetSeed: string;
|
||||||
|
mdFileCount: number;
|
||||||
|
mdMinSizeBytes: number;
|
||||||
|
mdMaxSizeBytes: number;
|
||||||
|
binFileCount: number;
|
||||||
|
binSizeBytes: number;
|
||||||
|
syncTimeoutSeconds: number;
|
||||||
|
requestedRttMs: number;
|
||||||
|
passphrase: string;
|
||||||
|
encrypt: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function readEnvString(name: string, fallback: string): string {
|
||||||
|
const value = Deno.env.get(name)?.trim();
|
||||||
|
return value && value.length > 0 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvNumber(name: string, fallback: number): number {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`${name} must be a positive number, got '${raw}'`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvBool(name: string, fallback: boolean): boolean {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return /^(1|true|yes|on)$/i.test(raw.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowMs(): number {
|
||||||
|
return performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMs(value: number): string {
|
||||||
|
return `${value.toFixed(1)} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number): string {
|
||||||
|
if (value < 1024) {
|
||||||
|
return `${value} B`;
|
||||||
|
}
|
||||||
|
const kib = value / 1024;
|
||||||
|
if (kib < 1024) {
|
||||||
|
return `${kib.toFixed(1)} KiB`;
|
||||||
|
}
|
||||||
|
return `${(kib / 1024).toFixed(1)} MiB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfig(): BenchmarkConfig {
|
||||||
|
return {
|
||||||
|
couchdbBackendUri: readEnvString("BENCH_COUCHDB_BACKEND_URI", "http://127.0.0.1:5989"),
|
||||||
|
couchdbProxyUri: readEnvString("BENCH_COUCHDB_URI", "http://127.0.0.1:15989"),
|
||||||
|
couchdbUser: readEnvString("BENCH_COUCHDB_USER", readEnvString("username", "admin")),
|
||||||
|
couchdbPassword: readEnvString("BENCH_COUCHDB_PASSWORD", readEnvString("password", "password")),
|
||||||
|
couchdbDbname: readEnvString("BENCH_COUCHDB_DBNAME", `bench-couchdb-${Date.now()}`),
|
||||||
|
datasetDirName: readEnvString("BENCH_DATASET_DIR", "bench-dataset"),
|
||||||
|
datasetSeed: readEnvString("BENCH_SEED", "livesync-benchmark-seed"),
|
||||||
|
mdFileCount: Math.floor(readEnvNumber("BENCH_MD_FILE_COUNT", 1500)),
|
||||||
|
mdMinSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MIN_SIZE_BYTES", 1024)),
|
||||||
|
mdMaxSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MAX_SIZE_BYTES", 20 * 1024)),
|
||||||
|
binFileCount: Math.floor(readEnvNumber("BENCH_BIN_FILE_COUNT", 500)),
|
||||||
|
binSizeBytes: Math.floor(readEnvNumber("BENCH_BIN_SIZE_BYTES", 100 * 1024)),
|
||||||
|
syncTimeoutSeconds: readEnvNumber("BENCH_SYNC_TIMEOUT", 240),
|
||||||
|
requestedRttMs: Math.floor(readEnvNumber("BENCH_COUCHDB_RTT_MS", 50)),
|
||||||
|
passphrase: readEnvString("BENCH_PASSPHRASE", `bench-${Date.now()}`),
|
||||||
|
encrypt: readEnvBool("BENCH_ENCRYPT", true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalResultPath(): string | undefined {
|
||||||
|
const raw = Deno.env.get("BENCH_RESULT_JSON")?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSampleFiles(entries: DatasetEntry[]): DatasetEntry[] {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const md = entries.find((e) => e.kind === "md");
|
||||||
|
const bin = entries.find((e) => e.kind === "bin");
|
||||||
|
const middle = entries[Math.floor(entries.length / 2)];
|
||||||
|
const last = entries[entries.length - 1];
|
||||||
|
const unique = new Map<string, DatasetEntry>();
|
||||||
|
for (const entry of [md, bin, middle, last]) {
|
||||||
|
if (entry) {
|
||||||
|
unique.set(entry.relativePath, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...unique.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyHandle = {
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
applied: boolean;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function startCouchdbProxy(options: { backendUri: string; proxyUri: string; requestedRttMs: number }): ProxyHandle {
|
||||||
|
const backend = new URL(options.backendUri);
|
||||||
|
const proxy = new URL(options.proxyUri);
|
||||||
|
const halfDelayMs = Math.max(1, Math.floor(options.requestedRttMs / 2));
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const listener = Deno.serve(
|
||||||
|
{
|
||||||
|
hostname: proxy.hostname,
|
||||||
|
port: Number(proxy.port),
|
||||||
|
signal: controller.signal,
|
||||||
|
onError(error) {
|
||||||
|
console.error(`[Proxy] ${String(error)}`);
|
||||||
|
return new Response("proxy error", { status: 502 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, halfDelayMs));
|
||||||
|
|
||||||
|
const targetUrl = new URL(request.url);
|
||||||
|
targetUrl.protocol = backend.protocol;
|
||||||
|
targetUrl.host = backend.host;
|
||||||
|
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
headers.delete("content-length");
|
||||||
|
|
||||||
|
let requestBody: ArrayBuffer | undefined;
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
try {
|
||||||
|
requestBody = await request.arrayBuffer();
|
||||||
|
} catch {
|
||||||
|
requestBody = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(targetUrl, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: requestBody,
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseHeaders = new Headers(upstream.headers);
|
||||||
|
responseHeaders.delete("content-length");
|
||||||
|
const responseBody = await upstream.arrayBuffer();
|
||||||
|
|
||||||
|
return new Response(responseBody, {
|
||||||
|
status: upstream.status,
|
||||||
|
statusText: upstream.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applied: true,
|
||||||
|
note: `local reverse proxy on ${proxy.origin} with ${halfDelayMs}ms pre-forward delay`,
|
||||||
|
stop: async () => {
|
||||||
|
controller.abort();
|
||||||
|
await listener.finished.catch(() => {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = buildConfig();
|
||||||
|
const resultPath = readOptionalResultPath();
|
||||||
|
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-couchdb-bench");
|
||||||
|
const vaultA = workDir.join("vault-a");
|
||||||
|
const vaultB = workDir.join("vault-b");
|
||||||
|
const settingsA = workDir.join("settings-a.json");
|
||||||
|
const settingsB = workDir.join("settings-b.json");
|
||||||
|
await Deno.mkdir(vaultA, { recursive: true });
|
||||||
|
await Deno.mkdir(vaultB, { recursive: true });
|
||||||
|
|
||||||
|
await initSettingsFile(settingsA);
|
||||||
|
await initSettingsFile(settingsB);
|
||||||
|
|
||||||
|
await startCouchdb(config.couchdbBackendUri, config.couchdbUser, config.couchdbPassword, config.couchdbDbname);
|
||||||
|
|
||||||
|
const proxy = startCouchdbProxy({
|
||||||
|
backendUri: config.couchdbBackendUri,
|
||||||
|
proxyUri: config.couchdbProxyUri,
|
||||||
|
requestedRttMs: config.requestedRttMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
applyRemoteSyncSettings(settingsA, {
|
||||||
|
remoteType: "COUCHDB",
|
||||||
|
couchdbUri: config.couchdbProxyUri,
|
||||||
|
couchdbUser: config.couchdbUser,
|
||||||
|
couchdbPassword: config.couchdbPassword,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
encrypt: config.encrypt,
|
||||||
|
passphrase: config.passphrase,
|
||||||
|
}),
|
||||||
|
applyRemoteSyncSettings(settingsB, {
|
||||||
|
remoteType: "COUCHDB",
|
||||||
|
couchdbUri: config.couchdbProxyUri,
|
||||||
|
couchdbUser: config.couchdbUser,
|
||||||
|
couchdbPassword: config.couchdbPassword,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
encrypt: config.encrypt,
|
||||||
|
passphrase: config.passphrase,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seedFiles = await createDeterministicDataset({
|
||||||
|
rootDir: vaultA,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
seed: config.datasetSeed,
|
||||||
|
mdCount: config.mdFileCount,
|
||||||
|
mdMinSizeBytes: config.mdMinSizeBytes,
|
||||||
|
mdMaxSizeBytes: config.mdMaxSizeBytes,
|
||||||
|
binCount: config.binFileCount,
|
||||||
|
binSizeBytes: config.binSizeBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mirrorStart = nowMs();
|
||||||
|
await runCliOrFail(vaultA, "--settings", settingsA, "mirror");
|
||||||
|
const mirrorElapsed = nowMs() - mirrorStart;
|
||||||
|
|
||||||
|
const syncAStart = nowMs();
|
||||||
|
await runCliOrFail(vaultA, "--settings", settingsA, "sync");
|
||||||
|
const syncAElapsed = nowMs() - syncAStart;
|
||||||
|
|
||||||
|
const syncBStart = nowMs();
|
||||||
|
await runCliOrFail(vaultB, "--settings", settingsB, "sync");
|
||||||
|
const syncBElapsed = nowMs() - syncBStart;
|
||||||
|
|
||||||
|
const sampleFiles = pickSampleFiles(seedFiles.entries);
|
||||||
|
for (const sample of sampleFiles) {
|
||||||
|
const pulledPath = workDir.join(`pulled-${sample.relativePath.split("/").join("_")}`);
|
||||||
|
await runCliOrFail(vaultB, "--settings", settingsB, "pull", sample.relativePath, pulledPath);
|
||||||
|
await assertFilesEqual(
|
||||||
|
sample.absolutePath,
|
||||||
|
pulledPath,
|
||||||
|
`sample file mismatch after CouchDB sync: ${sample.relativePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
mode: "couchdb-cli-benchmark",
|
||||||
|
couchdbBackendUri: config.couchdbBackendUri,
|
||||||
|
couchdbProxyUri: config.couchdbProxyUri,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
rttRequestedMs: config.requestedRttMs,
|
||||||
|
proxyApplied: proxy.applied,
|
||||||
|
proxyNote: proxy.note,
|
||||||
|
datasetSeed: config.datasetSeed,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
totalFiles: seedFiles.totalFiles,
|
||||||
|
totalBytes: seedFiles.totalBytes,
|
||||||
|
mdFileCount: seedFiles.mdCount,
|
||||||
|
binFileCount: seedFiles.binCount,
|
||||||
|
mirrorElapsedMs: Number(mirrorElapsed.toFixed(1)),
|
||||||
|
syncAElapsedMs: Number(syncAElapsed.toFixed(1)),
|
||||||
|
syncBElapsedMs: Number(syncBElapsed.toFixed(1)),
|
||||||
|
totalSyncElapsedMs: Number((syncAElapsed + syncBElapsed).toFixed(1)),
|
||||||
|
throughputBytesPerSec: Number((seedFiles.totalBytes / ((syncAElapsed + syncBElapsed) / 1000)).toFixed(2)),
|
||||||
|
throughputMiBPerSec: Number(
|
||||||
|
(seedFiles.totalBytes / ((syncAElapsed + syncBElapsed) / 1000) / 1024 / 1024).toFixed(4)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resultPath) {
|
||||||
|
await Deno.writeTextFile(resultPath, JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
console.error(
|
||||||
|
`[Benchmark] couchdb mirrored ${seedFiles.totalFiles} files (${formatBytes(seedFiles.totalBytes)}) in ${formatMs(
|
||||||
|
mirrorElapsed
|
||||||
|
)}, synced in ${formatMs(syncAElapsed + syncBElapsed)} (${result.throughputBytesPerSec} B/s, ${result.throughputMiBPerSec} MiB/s)`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await proxy.stop();
|
||||||
|
await stopCouchdb().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`[Fatal Error]`, error);
|
||||||
|
Deno.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { applyP2pSettings, applyP2pTestTweaks, initSettingsFile } from "./helpers/settings.ts";
|
||||||
|
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
||||||
|
import { discoverPeer, maybeStartLocalRelay, stopLocalRelayIfStarted } from "./helpers/p2p.ts";
|
||||||
|
import { assertFilesEqual, runCliOrFail } from "./helpers/cli.ts";
|
||||||
|
import { createDeterministicDataset, type DatasetEntry } from "./helpers/dataset.ts";
|
||||||
|
|
||||||
|
type BenchmarkConfig = {
|
||||||
|
relay: string;
|
||||||
|
appId: string;
|
||||||
|
roomId: string;
|
||||||
|
passphrase: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
datasetSeed: string;
|
||||||
|
mdFileCount: number;
|
||||||
|
mdMinSizeBytes: number;
|
||||||
|
mdMaxSizeBytes: number;
|
||||||
|
binFileCount: number;
|
||||||
|
binSizeBytes: number;
|
||||||
|
peersTimeoutSeconds: number;
|
||||||
|
syncTimeoutSeconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function readEnvString(name: string, fallback: string): string {
|
||||||
|
const value = Deno.env.get(name)?.trim();
|
||||||
|
return value && value.length > 0 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvNumber(name: string, fallback: number): number {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`${name} must be a positive number, got '${raw}'`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowMs(): number {
|
||||||
|
return performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMs(value: number): string {
|
||||||
|
return `${value.toFixed(1)} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number): string {
|
||||||
|
if (value < 1024) {
|
||||||
|
return `${value} B`;
|
||||||
|
}
|
||||||
|
const kib = value / 1024;
|
||||||
|
if (kib < 1024) {
|
||||||
|
return `${kib.toFixed(1)} KiB`;
|
||||||
|
}
|
||||||
|
const mib = kib / 1024;
|
||||||
|
return `${mib.toFixed(1)} MiB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfig(): BenchmarkConfig {
|
||||||
|
return {
|
||||||
|
relay: readEnvString("BENCH_RELAY", "ws://localhost:4000/"),
|
||||||
|
appId: readEnvString("BENCH_APP_ID", "self-hosted-livesync-cli-benchmark"),
|
||||||
|
roomId: readEnvString("BENCH_ROOM_ID", `bench-room-${Date.now()}`),
|
||||||
|
passphrase: readEnvString("BENCH_PASSPHRASE", `bench-${Date.now()}`),
|
||||||
|
datasetDirName: readEnvString("BENCH_DATASET_DIR", "bench-dataset"),
|
||||||
|
datasetSeed: readEnvString("BENCH_SEED", "livesync-benchmark-seed"),
|
||||||
|
mdFileCount: Math.floor(readEnvNumber("BENCH_MD_FILE_COUNT", 1500)),
|
||||||
|
mdMinSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MIN_SIZE_BYTES", 1024)),
|
||||||
|
mdMaxSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MAX_SIZE_BYTES", 20 * 1024)),
|
||||||
|
binFileCount: Math.floor(readEnvNumber("BENCH_BIN_FILE_COUNT", 500)),
|
||||||
|
binSizeBytes: Math.floor(readEnvNumber("BENCH_BIN_SIZE_BYTES", 100 * 1024)),
|
||||||
|
peersTimeoutSeconds: readEnvNumber("BENCH_PEERS_TIMEOUT", 20),
|
||||||
|
syncTimeoutSeconds: readEnvNumber("BENCH_SYNC_TIMEOUT", 240),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalResultPath(): string | undefined {
|
||||||
|
const raw = Deno.env.get("BENCH_RESULT_JSON")?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSampleFiles(entries: DatasetEntry[]): DatasetEntry[] {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const md = entries.find((e) => e.kind === "md");
|
||||||
|
const bin = entries.find((e) => e.kind === "bin");
|
||||||
|
const middle = entries[Math.floor(entries.length / 2)];
|
||||||
|
const last = entries[entries.length - 1];
|
||||||
|
const unique = new Map<string, DatasetEntry>();
|
||||||
|
for (const entry of [md, bin, middle, last]) {
|
||||||
|
if (entry) {
|
||||||
|
unique.set(entry.relativePath, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...unique.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = buildConfig();
|
||||||
|
const resultPath = readOptionalResultPath();
|
||||||
|
|
||||||
|
const relayStarted = await maybeStartLocalRelay(config.relay);
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-p2p-bench");
|
||||||
|
|
||||||
|
const hostVault = workDir.join("vault-host");
|
||||||
|
const clientVault = workDir.join("vault-client");
|
||||||
|
const hostSettings = workDir.join("settings-host.json");
|
||||||
|
const clientSettings = workDir.join("settings-client.json");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
Deno.mkdir(hostVault, { recursive: true }),
|
||||||
|
Deno.mkdir(clientVault, { recursive: true }),
|
||||||
|
initSettingsFile(hostSettings),
|
||||||
|
initSettingsFile(clientSettings),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
applyP2pSettings(hostSettings, config.roomId, config.passphrase, config.appId, config.relay, "~.*"),
|
||||||
|
applyP2pSettings(clientSettings, config.roomId, config.passphrase, config.appId, config.relay, "~.*"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
applyP2pTestTweaks(hostSettings, "p2p-bench-host", config.passphrase),
|
||||||
|
applyP2pTestTweaks(clientSettings, "p2p-bench-client", config.passphrase),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seedFiles = await createDeterministicDataset({
|
||||||
|
rootDir: hostVault,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
seed: config.datasetSeed,
|
||||||
|
mdCount: config.mdFileCount,
|
||||||
|
mdMinSizeBytes: config.mdMinSizeBytes,
|
||||||
|
mdMaxSizeBytes: config.mdMaxSizeBytes,
|
||||||
|
binCount: config.binFileCount,
|
||||||
|
binSizeBytes: config.binSizeBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mirrorStart = nowMs();
|
||||||
|
await runCliOrFail(hostVault, "--settings", hostSettings, "mirror");
|
||||||
|
const mirrorElapsed = nowMs() - mirrorStart;
|
||||||
|
|
||||||
|
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
||||||
|
try {
|
||||||
|
const hostReadyStart = nowMs();
|
||||||
|
await host.waitUntilContains("P2P host is running", 20000);
|
||||||
|
const hostReadyElapsed = nowMs() - hostReadyStart;
|
||||||
|
|
||||||
|
const peerDiscoveryStart = nowMs();
|
||||||
|
const peer = await discoverPeer(clientVault, clientSettings, config.peersTimeoutSeconds);
|
||||||
|
const peerDiscoveryElapsed = nowMs() - peerDiscoveryStart;
|
||||||
|
|
||||||
|
const syncStart = nowMs();
|
||||||
|
await runCliOrFail(
|
||||||
|
clientVault,
|
||||||
|
"--settings",
|
||||||
|
clientSettings,
|
||||||
|
"p2p-sync",
|
||||||
|
peer.id,
|
||||||
|
String(config.syncTimeoutSeconds)
|
||||||
|
);
|
||||||
|
const syncElapsed = nowMs() - syncStart;
|
||||||
|
|
||||||
|
const sampleFiles = pickSampleFiles(seedFiles.entries);
|
||||||
|
for (const sample of sampleFiles) {
|
||||||
|
const pulledPath = workDir.join(`pulled-${sample.relativePath.replaceAll("/", "_")}`);
|
||||||
|
await runCliOrFail(clientVault, "--settings", clientSettings, "pull", sample.relativePath, pulledPath);
|
||||||
|
await assertFilesEqual(
|
||||||
|
sample.absolutePath,
|
||||||
|
pulledPath,
|
||||||
|
`sample file mismatch after sync: ${sample.relativePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
mode: "p2p-cli-benchmark",
|
||||||
|
relay: config.relay,
|
||||||
|
appId: config.appId,
|
||||||
|
roomId: config.roomId,
|
||||||
|
datasetSeed: config.datasetSeed,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
peerId: peer.id,
|
||||||
|
peerName: peer.name,
|
||||||
|
totalFiles: seedFiles.totalFiles,
|
||||||
|
totalBytes: seedFiles.totalBytes,
|
||||||
|
mdFileCount: seedFiles.mdCount,
|
||||||
|
binFileCount: seedFiles.binCount,
|
||||||
|
mirrorElapsedMs: Number(mirrorElapsed.toFixed(1)),
|
||||||
|
hostReadyElapsedMs: Number(hostReadyElapsed.toFixed(1)),
|
||||||
|
peerDiscoveryElapsedMs: Number(peerDiscoveryElapsed.toFixed(1)),
|
||||||
|
syncElapsedMs: Number(syncElapsed.toFixed(1)),
|
||||||
|
throughputBytesPerSec: Number((seedFiles.totalBytes / (syncElapsed / 1000)).toFixed(2)),
|
||||||
|
throughputMiBPerSec: Number((seedFiles.totalBytes / (syncElapsed / 1000) / 1024 / 1024).toFixed(4)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resultPath) {
|
||||||
|
await Deno.writeTextFile(resultPath, JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
console.error(
|
||||||
|
`[Benchmark] mirrored ${seedFiles.totalFiles} files (${formatBytes(seedFiles.totalBytes)}) in ${formatMs(mirrorElapsed)}, ` +
|
||||||
|
`synced in ${formatMs(syncElapsed)} ` +
|
||||||
|
`(${result.throughputBytesPerSec} B/s, ${result.throughputMiBPerSec} MiB/s)`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await host.stop();
|
||||||
|
await stopLocalRelayIfStarted(relayStarted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`[Fatal Error]`, error);
|
||||||
|
Deno.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RESULTS_ROOT="${SCRIPT_DIR}/bench-results"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUT_DIR="${RESULTS_ROOT}/${TIMESTAMP}"
|
||||||
|
|
||||||
|
mkdir -p "${OUT_DIR}"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] output directory: ${OUT_DIR}"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] running p2p benchmark"
|
||||||
|
(
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
|
BENCH_RESULT_JSON="${OUT_DIR}/p2p.json" deno task bench:p2p
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "[bench-wrapper] running couchdb benchmark with RTT ${BENCH_COUCHDB_RTT_MS:-default} ms (emulating HTTP network latency)"
|
||||||
|
(
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
|
BENCH_RESULT_JSON="${OUT_DIR}/couchdb.json" deno task bench:couchdb
|
||||||
|
)
|
||||||
|
|
||||||
|
cat > "${OUT_DIR}/README.txt" <<EOF
|
||||||
|
Bench wrapper result set
|
||||||
|
|
||||||
|
Generated at: ${TIMESTAMP}
|
||||||
|
Directory: ${OUT_DIR}
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- p2p.json
|
||||||
|
- couchdb.json
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[bench-wrapper] verify outputs by cat"
|
||||||
|
echo "========== ${OUT_DIR}/README.txt =========="
|
||||||
|
cat "${OUT_DIR}/README.txt"
|
||||||
|
echo "========== ${OUT_DIR}/p2p.json =========="
|
||||||
|
cat "${OUT_DIR}/p2p.json"
|
||||||
|
echo "========== ${OUT_DIR}/couchdb.json =========="
|
||||||
|
cat "${OUT_DIR}/couchdb.json"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] done"
|
||||||
|
echo "[bench-wrapper] result directory: ${OUT_DIR}"
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"test": "deno test --env-file=.test.env -A --no-check test-*.ts",
|
"test": "deno test --env-file=.test.env -A --no-check test-*.ts",
|
||||||
"test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts",
|
"test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts test-daemon.ts",
|
||||||
|
"test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts",
|
||||||
|
"test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts",
|
||||||
|
"test:remote-commands": "deno test --env-file=.test.env -A --no-check test-remote-commands.ts",
|
||||||
"test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
|
"test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
|
||||||
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
||||||
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
|
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
|
||||||
@@ -12,8 +15,16 @@
|
|||||||
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
|
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
|
||||||
"test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
|
"test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
|
||||||
"test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
|
"test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
|
||||||
|
"bench:p2p": "deno run --env-file=.test.env -A --no-check bench-p2p.ts",
|
||||||
|
"bench:couchdb": "deno run --env-file=.test.env -A --no-check bench-couchdb.ts",
|
||||||
|
"bench:item1": "bash ./bench-run-item1.sh",
|
||||||
|
"bench:item1:full": "BENCH_MD_FILE_COUNT=1500 BENCH_MD_MIN_SIZE_BYTES=1024 BENCH_MD_MAX_SIZE_BYTES=20480 BENCH_BIN_FILE_COUNT=500 BENCH_BIN_SIZE_BYTES=102400 BENCH_COUCHDB_RTT_MS=50 bash ./bench-run-item1.sh",
|
||||||
"test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
|
"test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
|
||||||
"test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts"
|
"test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts",
|
||||||
|
"test:e2e-matrix:couchdb-enc0": "deno test --env-file=.test.env -A --no-check --filter='e2e matrix: COUCHDB-enc0' test-e2e-two-vaults-matrix.ts",
|
||||||
|
"test:e2e-matrix:couchdb-enc1": "deno test --env-file=.test.env -A --no-check --filter='e2e matrix: COUCHDB-enc1' test-e2e-two-vaults-matrix.ts",
|
||||||
|
"test:e2e-matrix:minio-enc0": "deno test --env-file=.test.env -A --no-check --filter='e2e matrix: MINIO-enc0' test-e2e-two-vaults-matrix.ts",
|
||||||
|
"test:e2e-matrix:minio-enc1": "deno test --env-file=.test.env -A --no-check --filter='e2e matrix: MINIO-enc1' test-e2e-two-vaults-matrix.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@std/assert": "jsr:@std/assert@^1.0.13",
|
"@std/assert": "jsr:@std/assert@^1.0.13",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CLI_DIR } from "./cli.ts";
|
import { CLI_DIR, TEE_ENABLED, formatTeeCommand, createLineTeeWriter } from "./cli.ts";
|
||||||
import { join } from "@std/path";
|
import { join } from "@std/path";
|
||||||
|
|
||||||
const CLI_DIST = join(CLI_DIR, "dist", "index.cjs");
|
const CLI_DIST = join(CLI_DIR, "dist", "index.cjs");
|
||||||
@@ -12,10 +12,9 @@ function decorateArgs(args: string[]): string[] {
|
|||||||
async function pump(
|
async function pump(
|
||||||
stream: ReadableStream<Uint8Array>,
|
stream: ReadableStream<Uint8Array>,
|
||||||
sink: (text: string) => void,
|
sink: (text: string) => void,
|
||||||
teeTarget: WritableStream<Uint8Array> | null
|
teeTarget: { write: (chunk: Uint8Array) => void; close: () => void } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reader = stream.getReader();
|
const reader = stream.getReader();
|
||||||
const writer = teeTarget?.getWriter();
|
|
||||||
const dec = new TextDecoder();
|
const dec = new TextDecoder();
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -23,12 +22,12 @@ async function pump(
|
|||||||
if (done) break;
|
if (done) break;
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
sink(dec.decode(value, { stream: true }));
|
sink(dec.decode(value, { stream: true }));
|
||||||
if (writer) {
|
if (teeTarget) {
|
||||||
await writer.write(value);
|
teeTarget.write(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (writer) writer.releaseLock();
|
if (teeTarget) teeTarget.close();
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,19 +42,20 @@ export class BackgroundCliProcess {
|
|||||||
readonly child: Deno.ChildProcess,
|
readonly child: Deno.ChildProcess,
|
||||||
readonly args: string[]
|
readonly args: string[]
|
||||||
) {
|
) {
|
||||||
|
const cliArgs = decorateArgs(args);
|
||||||
this.#stdoutDone = pump(
|
this.#stdoutDone = pump(
|
||||||
child.stdout,
|
child.stdout,
|
||||||
(text) => {
|
(text) => {
|
||||||
this.#stdout += text;
|
this.#stdout += text;
|
||||||
},
|
},
|
||||||
null
|
TEE_ENABLED ? createLineTeeWriter(child.pid, "stdout", (chunk) => Deno.stdout.writeSync(chunk)) : null
|
||||||
);
|
);
|
||||||
this.#stderrDone = pump(
|
this.#stderrDone = pump(
|
||||||
child.stderr,
|
child.stderr,
|
||||||
(text) => {
|
(text) => {
|
||||||
this.#stderr += text;
|
this.#stderr += text;
|
||||||
},
|
},
|
||||||
null
|
TEE_ENABLED ? createLineTeeWriter(child.pid, "stderr", (chunk) => Deno.stderr.writeSync(chunk)) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,9 @@ export class BackgroundCliProcess {
|
|||||||
if (this.combined.includes(needle)) return;
|
if (this.combined.includes(needle)) return;
|
||||||
const status = await Promise.race([
|
const status = await Promise.race([
|
||||||
this.child.status.then((s) => ({ type: "status" as const, status: s })),
|
this.child.status.then((s) => ({ type: "status" as const, status: s })),
|
||||||
new Promise<{ type: "tick" }>((resolve) => setTimeout(() => resolve({ type: "tick" }), 100)),
|
new Promise<{ type: "tick" }>((resolve) =>
|
||||||
|
setTimeout(() => resolve({ type: "tick" }), 100)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
if (status.type === "status") {
|
if (status.type === "status") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -101,12 +103,20 @@ export class BackgroundCliProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startCliInBackground(...args: string[]): BackgroundCliProcess {
|
export function startCliInBackground(...args: string[]): BackgroundCliProcess {
|
||||||
|
const cliArgs = decorateArgs(args);
|
||||||
const child = new Deno.Command("node", {
|
const child = new Deno.Command("node", {
|
||||||
args: [CLI_DIST, ...decorateArgs(args)],
|
args: [CLI_DIST, ...cliArgs],
|
||||||
cwd: CLI_DIR,
|
cwd: CLI_DIR,
|
||||||
stdin: "null",
|
stdin: "null",
|
||||||
stdout: "piped",
|
stdout: "piped",
|
||||||
stderr: "piped",
|
stderr: "piped",
|
||||||
}).spawn();
|
}).spawn();
|
||||||
|
|
||||||
|
if (TEE_ENABLED) {
|
||||||
|
Deno.stdout.writeSync(
|
||||||
|
new TextEncoder().encode(`[CLI tee pid=${child.pid}] process(bg): ${formatTeeCommand(cliArgs)}\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new BackgroundCliProcess(child, args);
|
return new BackgroundCliProcess(child, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface CliResult {
|
|||||||
code: number;
|
code: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEE_ENABLED = Deno.env.get("LIVESYNC_TEST_TEE") === "1";
|
export const TEE_ENABLED = Deno.env.get("LIVESYNC_TEST_TEE") === "1";
|
||||||
const VERBOSE_ENABLED = Deno.env.get("LIVESYNC_CLI_VERBOSE") === "1";
|
const VERBOSE_ENABLED = Deno.env.get("LIVESYNC_CLI_VERBOSE") === "1";
|
||||||
const DEBUG_ENABLED = Deno.env.get("LIVESYNC_CLI_DEBUG") === "1";
|
const DEBUG_ENABLED = Deno.env.get("LIVESYNC_CLI_DEBUG") === "1";
|
||||||
|
|
||||||
@@ -39,27 +39,73 @@ function concatChunks(chunks: Uint8Array[]): Uint8Array {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTeeCommand(args: string[]): string {
|
||||||
|
return ["node", CLI_DIST, ...args].map((part) => JSON.stringify(part)).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLineTeeWriter(
|
||||||
|
pid: number,
|
||||||
|
streamName: "stdout" | "stderr",
|
||||||
|
writer: (chunk: Uint8Array) => void
|
||||||
|
): { write: (chunk: Uint8Array) => void; close: () => void } {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
let pending = "";
|
||||||
|
let headerWritten = false;
|
||||||
|
const emitLine = (line: string) => {
|
||||||
|
if (!headerWritten) {
|
||||||
|
writer(enc.encode(`[CLI tee pid=${pid}:${streamName}]\n`));
|
||||||
|
headerWritten = true;
|
||||||
|
}
|
||||||
|
writer(enc.encode(`[CLI tee pid=${pid}:${streamName}] ${line}\n`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const flush = (final = false) => {
|
||||||
|
let index = pending.indexOf("\n");
|
||||||
|
while (index >= 0) {
|
||||||
|
const line = pending.slice(0, index).replace(/\r$/, "");
|
||||||
|
pending = pending.slice(index + 1);
|
||||||
|
emitLine(line);
|
||||||
|
index = pending.indexOf("\n");
|
||||||
|
}
|
||||||
|
if (final && pending.length > 0) {
|
||||||
|
emitLine(pending.replace(/\r$/, ""));
|
||||||
|
pending = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
write(chunk: Uint8Array) {
|
||||||
|
pending += dec.decode(chunk, { stream: true });
|
||||||
|
flush(false);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
pending += dec.decode();
|
||||||
|
flush(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function collectStream(
|
async function collectStream(
|
||||||
stream: ReadableStream<Uint8Array>,
|
stream: ReadableStream<Uint8Array>,
|
||||||
teeTarget: WritableStream<Uint8Array> | null
|
teeTarget: { write: (chunk: Uint8Array) => void; close: () => void } | null
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const reader = stream.getReader();
|
const reader = stream.getReader();
|
||||||
const chunks: Uint8Array[] = [];
|
const chunks: Uint8Array[] = [];
|
||||||
const writer = teeTarget?.getWriter();
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
if (value) {
|
if (value) {
|
||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
if (writer) {
|
if (teeTarget) {
|
||||||
await writer.write(value);
|
teeTarget.write(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (writer) {
|
if (teeTarget) {
|
||||||
writer.releaseLock();
|
teeTarget.close();
|
||||||
}
|
}
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
@@ -76,8 +122,20 @@ async function runNodeCommand(args: string[], stdinData?: Uint8Array): Promise<C
|
|||||||
stderr: "piped",
|
stderr: "piped",
|
||||||
}).spawn();
|
}).spawn();
|
||||||
|
|
||||||
const stdoutPromise = collectStream(child.stdout, TEE_ENABLED ? Deno.stdout.writable : null);
|
if (TEE_ENABLED) {
|
||||||
const stderrPromise = collectStream(child.stderr, TEE_ENABLED ? Deno.stderr.writable : null);
|
Deno.stdout.writeSync(
|
||||||
|
new TextEncoder().encode(`[CLI tee pid=${child.pid}] process: ${formatTeeCommand(cliArgs)}\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdoutPromise = collectStream(
|
||||||
|
child.stdout,
|
||||||
|
TEE_ENABLED ? createLineTeeWriter(child.pid, "stdout", (chunk) => Deno.stdout.writeSync(chunk)) : null
|
||||||
|
);
|
||||||
|
const stderrPromise = collectStream(
|
||||||
|
child.stderr,
|
||||||
|
TEE_ENABLED ? createLineTeeWriter(child.pid, "stderr", (chunk) => Deno.stderr.writeSync(chunk)) : null
|
||||||
|
);
|
||||||
|
|
||||||
if (stdinData) {
|
if (stdinData) {
|
||||||
const w = child.stdin.getWriter();
|
const w = child.stdin.getWriter();
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
export type DeterministicDatasetConfig = {
|
||||||
|
rootDir: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
seed: string;
|
||||||
|
mdCount: number;
|
||||||
|
mdMinSizeBytes: number;
|
||||||
|
mdMaxSizeBytes: number;
|
||||||
|
binCount: number;
|
||||||
|
binSizeBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DatasetEntry = {
|
||||||
|
kind: "md" | "bin";
|
||||||
|
relativePath: string;
|
||||||
|
absolutePath: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeterministicDataset = {
|
||||||
|
rootDir: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
seed: string;
|
||||||
|
entries: DatasetEntry[];
|
||||||
|
totalFiles: number;
|
||||||
|
totalBytes: number;
|
||||||
|
mdCount: number;
|
||||||
|
binCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function fnv1a32(input: string): number {
|
||||||
|
let hash = 0x811c9dc5;
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
hash ^= input.charCodeAt(i) & 0xff;
|
||||||
|
hash = Math.imul(hash, 0x01000193);
|
||||||
|
}
|
||||||
|
return hash >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createXorshift32(seed: number): () => number {
|
||||||
|
let state = seed >>> 0;
|
||||||
|
if (state === 0) {
|
||||||
|
state = 0x9e3779b9;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
state ^= state << 13;
|
||||||
|
state ^= state >>> 17;
|
||||||
|
state ^= state << 5;
|
||||||
|
return state >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTextBytes(size: number, fileIndex: number, seed: string): Uint8Array {
|
||||||
|
const template =
|
||||||
|
`# Bench file ${fileIndex}\n` +
|
||||||
|
`seed: ${seed}\n` +
|
||||||
|
"lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n";
|
||||||
|
|
||||||
|
const templateBytes = new TextEncoder().encode(template);
|
||||||
|
const out = new Uint8Array(size);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
out[i] = templateBytes[i % templateBytes.length];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPath(rootDir: string, relativePath: string): string {
|
||||||
|
return `${rootDir}/${relativePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDeterministicDataset(config: DeterministicDatasetConfig): Promise<DeterministicDataset> {
|
||||||
|
if (config.mdCount < 0 || config.binCount < 0) {
|
||||||
|
throw new Error("mdCount and binCount must be non-negative");
|
||||||
|
}
|
||||||
|
if (config.mdMinSizeBytes <= 0 || config.mdMaxSizeBytes <= 0 || config.binSizeBytes <= 0) {
|
||||||
|
throw new Error("all size values must be positive");
|
||||||
|
}
|
||||||
|
if (config.mdMinSizeBytes > config.mdMaxSizeBytes) {
|
||||||
|
throw new Error("mdMinSizeBytes must be <= mdMaxSizeBytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasetRoot = toPath(config.rootDir, config.datasetDirName);
|
||||||
|
const mdDir = `${datasetRoot}/md`;
|
||||||
|
const binDir = `${datasetRoot}/bin`;
|
||||||
|
await Deno.mkdir(mdDir, { recursive: true });
|
||||||
|
await Deno.mkdir(binDir, { recursive: true });
|
||||||
|
|
||||||
|
const nextRandom = createXorshift32(fnv1a32(config.seed));
|
||||||
|
const mdRange = config.mdMaxSizeBytes - config.mdMinSizeBytes + 1;
|
||||||
|
const entries: DatasetEntry[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < config.mdCount; index++) {
|
||||||
|
const size = config.mdMinSizeBytes + (nextRandom() % mdRange);
|
||||||
|
const relativePath = `${config.datasetDirName}/md/file-${String(index).padStart(4, "0")}.md`;
|
||||||
|
const absolutePath = toPath(config.rootDir, relativePath);
|
||||||
|
const body = createTextBytes(size, index, config.seed);
|
||||||
|
await Deno.writeFile(absolutePath, body);
|
||||||
|
entries.push({ kind: "md", relativePath, absolutePath, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < config.binCount; index++) {
|
||||||
|
const size = config.binSizeBytes;
|
||||||
|
const relativePath = `${config.datasetDirName}/bin/file-${String(index).padStart(4, "0")}.bin`;
|
||||||
|
const absolutePath = toPath(config.rootDir, relativePath);
|
||||||
|
const body = new Uint8Array(size);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
body[i] = nextRandom() & 0xff;
|
||||||
|
}
|
||||||
|
await Deno.writeFile(absolutePath, body);
|
||||||
|
entries.push({ kind: "bin", relativePath, absolutePath, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBytes = entries.reduce((sum, e) => sum + e.size, 0);
|
||||||
|
return {
|
||||||
|
rootDir: config.rootDir,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
seed: config.seed,
|
||||||
|
entries,
|
||||||
|
totalFiles: entries.length,
|
||||||
|
totalBytes,
|
||||||
|
mdCount: config.mdCount,
|
||||||
|
binCount: config.binCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@ type DockerInvoker = {
|
|||||||
|
|
||||||
let dockerInvokerPromise: Promise<DockerInvoker> | null = null;
|
let dockerInvokerPromise: Promise<DockerInvoker> | null = null;
|
||||||
const DOCKER_TEE = Deno.env.get("LIVESYNC_DOCKER_TEE") === "1" || Deno.env.get("LIVESYNC_TEST_TEE") === "1";
|
const DOCKER_TEE = Deno.env.get("LIVESYNC_DOCKER_TEE") === "1" || Deno.env.get("LIVESYNC_TEST_TEE") === "1";
|
||||||
|
const trackedContainers = new Set<string>();
|
||||||
|
const CLEANUP_SIGNALS: Deno.Signal[] = ["SIGINT", "SIGTERM"];
|
||||||
|
let signalCleanupHandlersInstalled = false;
|
||||||
|
let signalCleanupInProgress = false;
|
||||||
|
const signalCleanupHandlers = new Map<Deno.Signal, () => void>();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Low-level docker wrapper
|
// Low-level docker wrapper
|
||||||
@@ -27,29 +32,53 @@ function parseCommand(command: string): { bin: string; prefix: string[] } {
|
|||||||
return { bin: parts[0], prefix: parts.slice(1) };
|
return { bin: parts[0], prefix: parts.slice(1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCommand(bin: string, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
async function collectStream(
|
||||||
const cmd = new Deno.Command(bin, {
|
stream: ReadableStream<Uint8Array>,
|
||||||
args,
|
teeTarget: ((chunk: Uint8Array) => void) | null
|
||||||
stdin: "null",
|
): Promise<Uint8Array> {
|
||||||
stdout: "piped",
|
const reader = stream.getReader();
|
||||||
stderr: "piped",
|
const chunks: Uint8Array[] = [];
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
const { code, stdout, stderr } = await cmd.output();
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (!value) continue;
|
||||||
|
chunks.push(value);
|
||||||
|
if (teeTarget) {
|
||||||
|
teeTarget(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||||
|
const out = new Uint8Array(total);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
out.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(bin: string, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||||
|
try {
|
||||||
|
const child = new Deno.Command(bin, {
|
||||||
|
args,
|
||||||
|
stdin: "null",
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "piped",
|
||||||
|
}).spawn();
|
||||||
|
const stdoutPromise = collectStream(child.stdout, DOCKER_TEE ? (chunk) => Deno.stdout.writeSync(chunk) : null);
|
||||||
|
const stderrPromise = collectStream(child.stderr, DOCKER_TEE ? (chunk) => Deno.stderr.writeSync(chunk) : null);
|
||||||
|
const [status, stdout, stderr] = await Promise.all([child.status, stdoutPromise, stderrPromise]);
|
||||||
const dec = new TextDecoder();
|
const dec = new TextDecoder();
|
||||||
const result = {
|
const result = {
|
||||||
code,
|
code: status.code,
|
||||||
stdout: dec.decode(stdout),
|
stdout: dec.decode(stdout),
|
||||||
stderr: dec.decode(stderr),
|
stderr: dec.decode(stderr),
|
||||||
};
|
};
|
||||||
if (DOCKER_TEE) {
|
|
||||||
if (result.stdout.trim().length > 0) {
|
|
||||||
console.log(`[docker:${bin}] ${result.stdout.trimEnd()}`);
|
|
||||||
}
|
|
||||||
if (result.stderr.trim().length > 0) {
|
|
||||||
console.error(`[docker:${bin}] ${result.stderr.trimEnd()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Deno.errors.NotFound) {
|
if (err instanceof Deno.errors.NotFound) {
|
||||||
@@ -159,6 +188,73 @@ async function dockerOrFail(...args: string[]): Promise<string> {
|
|||||||
return r.stdout;
|
return r.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopAndRemoveContainer(container: string): Promise<void> {
|
||||||
|
await docker("stop", container).catch(() => {});
|
||||||
|
await docker("rm", container).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupTrackedContainers(reason: string): Promise<void> {
|
||||||
|
const names = [...trackedContainers];
|
||||||
|
if (names.length === 0) return;
|
||||||
|
|
||||||
|
console.warn(`[WARN] cleaning up tracked containers on ${reason}: ${names.join(", ")}`);
|
||||||
|
for (const container of names.reverse()) {
|
||||||
|
await stopAndRemoveContainer(container);
|
||||||
|
trackedContainers.delete(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSignalCleanup(signal: Deno.Signal): Promise<void> {
|
||||||
|
if (signalCleanupInProgress) return;
|
||||||
|
signalCleanupInProgress = true;
|
||||||
|
try {
|
||||||
|
await cleanupTrackedContainers(`signal ${signal}`);
|
||||||
|
} finally {
|
||||||
|
Deno.exit(signal === "SIGINT" ? 130 : 143);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSignalCleanupHandlers(): void {
|
||||||
|
if (signalCleanupHandlersInstalled) return;
|
||||||
|
signalCleanupHandlersInstalled = true;
|
||||||
|
for (const signal of CLEANUP_SIGNALS) {
|
||||||
|
const listener = () => {
|
||||||
|
void handleSignalCleanup(signal);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
Deno.addSignalListener(signal, listener);
|
||||||
|
signalCleanupHandlers.set(signal, listener);
|
||||||
|
} catch {
|
||||||
|
// Unsupported signal on this platform.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSignalCleanupHandlers(): void {
|
||||||
|
if (!signalCleanupHandlersInstalled) return;
|
||||||
|
for (const [signal, listener] of signalCleanupHandlers) {
|
||||||
|
try {
|
||||||
|
Deno.removeSignalListener(signal, listener);
|
||||||
|
} catch {
|
||||||
|
// Ignore if already removed or unsupported.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signalCleanupHandlers.clear();
|
||||||
|
signalCleanupHandlersInstalled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackContainer(container: string): void {
|
||||||
|
ensureSignalCleanupHandlers();
|
||||||
|
trackedContainers.add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function untrackContainer(container: string): void {
|
||||||
|
trackedContainers.delete(container);
|
||||||
|
if (trackedContainers.size === 0) {
|
||||||
|
removeSignalCleanupHandlers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -235,8 +331,8 @@ const MINIO_IMAGE = "minio/minio";
|
|||||||
const MINIO_MC_IMAGE = "minio/mc";
|
const MINIO_MC_IMAGE = "minio/mc";
|
||||||
|
|
||||||
export async function stopCouchdb(): Promise<void> {
|
export async function stopCouchdb(): Promise<void> {
|
||||||
await docker("stop", COUCHDB_CONTAINER);
|
await stopAndRemoveContainer(COUCHDB_CONTAINER);
|
||||||
await docker("rm", COUCHDB_CONTAINER);
|
untrackContainer(COUCHDB_CONTAINER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,6 +361,7 @@ export async function startCouchdb(couchdbUri: string, user: string, password: s
|
|||||||
"COUCHDB_SINGLE_NODE=y",
|
"COUCHDB_SINGLE_NODE=y",
|
||||||
COUCHDB_IMAGE
|
COUCHDB_IMAGE
|
||||||
);
|
);
|
||||||
|
trackContainer(COUCHDB_CONTAINER);
|
||||||
|
|
||||||
console.log("[INFO] initialising CouchDB");
|
console.log("[INFO] initialising CouchDB");
|
||||||
await initCouchdb(couchdbUri, user, password);
|
await initCouchdb(couchdbUri, user, password);
|
||||||
@@ -365,8 +462,8 @@ function shQuote(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function stopMinio(): Promise<void> {
|
export async function stopMinio(): Promise<void> {
|
||||||
await docker("stop", MINIO_CONTAINER);
|
await stopAndRemoveContainer(MINIO_CONTAINER);
|
||||||
await docker("rm", MINIO_CONTAINER);
|
untrackContainer(MINIO_CONTAINER);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initMinioBucket(
|
async function initMinioBucket(
|
||||||
@@ -446,6 +543,7 @@ export async function startMinio(
|
|||||||
"--console-address",
|
"--console-address",
|
||||||
":9001"
|
":9001"
|
||||||
);
|
);
|
||||||
|
trackContainer(MINIO_CONTAINER);
|
||||||
|
|
||||||
console.log(`[INFO] initialising MinIO test bucket: ${bucket}`);
|
console.log(`[INFO] initialising MinIO test bucket: ${bucket}`);
|
||||||
let initialised = false;
|
let initialised = false;
|
||||||
@@ -493,8 +591,8 @@ EOF
|
|||||||
exec /app/strfry --config /tmp/strfry.conf relay`;
|
exec /app/strfry --config /tmp/strfry.conf relay`;
|
||||||
|
|
||||||
export async function stopP2pRelay(): Promise<void> {
|
export async function stopP2pRelay(): Promise<void> {
|
||||||
await docker("stop", P2P_RELAY_CONTAINER);
|
await stopAndRemoveContainer(P2P_RELAY_CONTAINER);
|
||||||
await docker("rm", P2P_RELAY_CONTAINER);
|
untrackContainer(P2P_RELAY_CONTAINER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -523,8 +621,51 @@ export async function startP2pRelay(): Promise<void> {
|
|||||||
"-lc",
|
"-lc",
|
||||||
STRFRY_BOOTSTRAP_SH
|
STRFRY_BOOTSTRAP_SH
|
||||||
);
|
);
|
||||||
|
trackContainer(P2P_RELAY_CONTAINER);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLocalP2pRelay(relayUrl: string): boolean {
|
export function isLocalP2pRelay(relayUrl: string): boolean {
|
||||||
return relayUrl === "ws://localhost:4000" || relayUrl === "ws://localhost:4000/";
|
return relayUrl.includes("localhost") || relayUrl.includes("127.0.0.1") || relayUrl.includes("[::1]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Coturn (STUN/TURN)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const COTURN_CONTAINER = "coturn-test";
|
||||||
|
const COTURN_IMAGE = "coturn/coturn:latest";
|
||||||
|
|
||||||
|
export async function stopCoturn(): Promise<void> {
|
||||||
|
await stopAndRemoveContainer(COTURN_CONTAINER);
|
||||||
|
untrackContainer(COTURN_CONTAINER);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startCoturn(
|
||||||
|
port = 3478,
|
||||||
|
user = "testuser",
|
||||||
|
pass = "testpass",
|
||||||
|
realm = "livesync.test"
|
||||||
|
): Promise<void> {
|
||||||
|
console.log("[INFO] stopping leftover Coturn container if present");
|
||||||
|
await stopCoturn().catch(() => {});
|
||||||
|
|
||||||
|
const { getOptimalLoopbackIp } = await import("./net.ts");
|
||||||
|
const externalIp = await getOptimalLoopbackIp();
|
||||||
|
|
||||||
|
console.log(`[INFO] starting local Coturn container with external-ip ${externalIp}`);
|
||||||
|
await dockerOrFail(
|
||||||
|
"run",
|
||||||
|
"-d",
|
||||||
|
"--name",
|
||||||
|
COTURN_CONTAINER,
|
||||||
|
"-p",
|
||||||
|
`${port}:${port}`,
|
||||||
|
"-p",
|
||||||
|
`${port}:${port}/udp`,
|
||||||
|
COTURN_IMAGE,
|
||||||
|
"--log-file=stdout",
|
||||||
|
`--external-ip=${externalIp}`,
|
||||||
|
`--user=${user}:${pass}`,
|
||||||
|
`--realm=${realm}`
|
||||||
|
);
|
||||||
|
trackContainer(COTURN_CONTAINER);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
type WaitForPortOptions = {
|
||||||
|
timeoutMs?: number;
|
||||||
|
intervalMs?: number;
|
||||||
|
connectTimeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWithTimeout(hostname: string, port: number, timeoutMs: number): Promise<void> {
|
||||||
|
let timer: number | undefined;
|
||||||
|
try {
|
||||||
|
const connPromise = Deno.connect({ hostname, port });
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timer = setTimeout(() => reject(new Error(`connect timeout after ${timeoutMs}ms`)), timeoutMs);
|
||||||
|
});
|
||||||
|
const conn = await Promise.race([connPromise, timeoutPromise]);
|
||||||
|
conn.close();
|
||||||
|
} finally {
|
||||||
|
if (timer !== undefined) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForPort(hostname: string, port: number, options: WaitForPortOptions = {}): Promise<void> {
|
||||||
|
const timeoutMs = options.timeoutMs ?? 15000;
|
||||||
|
const intervalMs = options.intervalMs ?? 250;
|
||||||
|
const connectTimeoutMs = options.connectTimeoutMs ?? 1000;
|
||||||
|
|
||||||
|
const started = Date.now();
|
||||||
|
let lastError: unknown;
|
||||||
|
|
||||||
|
while (Date.now() - started < timeoutMs) {
|
||||||
|
try {
|
||||||
|
await connectWithTimeout(hostname, port, connectTimeoutMs);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await sleep(intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Port ${hostname}:${port} did not become ready within ${timeoutMs}ms` +
|
||||||
|
(lastError ? ` (last error: ${String(lastError)})` : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOptimalLoopbackIp(): Promise<string> {
|
||||||
|
const ipv4 = "127.0.0.1";
|
||||||
|
const ipv6 = "::1";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const l = Deno.listen({ hostname: ipv4, port: 0 });
|
||||||
|
l.close();
|
||||||
|
return ipv4;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const l = Deno.listen({ hostname: ipv6, port: 0 });
|
||||||
|
l.close();
|
||||||
|
return ipv6;
|
||||||
|
} catch {
|
||||||
|
return ipv4; // fallback to default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
import { runCli } from "./cli.ts";
|
import { runCli } from "./cli.ts";
|
||||||
import { isLocalP2pRelay, startP2pRelay, stopP2pRelay } from "./docker.ts";
|
import { isLocalP2pRelay, startP2pRelay, stopP2pRelay, startCoturn, stopCoturn } from "./docker.ts";
|
||||||
|
import { waitForPort } from "./net.ts";
|
||||||
|
|
||||||
export type PeerEntry = {
|
export type PeerEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRelayEndpoint(relay: string): { hostname: string; port: number } {
|
||||||
|
const url = new URL(relay);
|
||||||
|
const port = url.port ? Number(url.port) : url.protocol === "ws:" ? 80 : url.protocol === "wss:" ? 443 : NaN;
|
||||||
|
if (!Number.isFinite(port)) {
|
||||||
|
throw new Error(`Unsupported relay URL: ${relay}`);
|
||||||
|
}
|
||||||
|
const hostname = url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
|
||||||
|
return { hostname, port };
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePeerLines(output: string): PeerEntry[] {
|
export function parsePeerLines(output: string): PeerEntry[] {
|
||||||
return output
|
return output
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
@@ -20,28 +35,58 @@ export async function discoverPeer(
|
|||||||
timeoutSeconds: number,
|
timeoutSeconds: number,
|
||||||
targetPeer?: string
|
targetPeer?: string
|
||||||
): Promise<PeerEntry> {
|
): Promise<PeerEntry> {
|
||||||
const result = await runCli(vaultDir, "--settings", settingsFile, "p2p-peers", String(timeoutSeconds));
|
const retries = Math.max(0, Number(Deno.env.get("LIVESYNC_P2P_PEERS_RETRY") ?? "3"));
|
||||||
if (result.code !== 0) {
|
let lastCombined = "";
|
||||||
throw new Error(`p2p-peers failed\n${result.combined}`);
|
|
||||||
}
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||||
const peers = parsePeerLines(result.stdout);
|
const result = await runCli(vaultDir, "--settings", settingsFile, "p2p-peers", String(timeoutSeconds));
|
||||||
if (targetPeer) {
|
lastCombined = result.combined;
|
||||||
const matched = peers.find((peer) => peer.id === targetPeer || peer.name === targetPeer);
|
|
||||||
if (matched) return matched;
|
if (result.code === 0) {
|
||||||
}
|
const peers = parsePeerLines(result.stdout);
|
||||||
if (peers.length === 0) {
|
if (targetPeer) {
|
||||||
const fallback = result.combined.match(/Advertisement from\s+([^\s]+)/);
|
const matched = peers.find((peer) => peer.id === targetPeer || peer.name === targetPeer);
|
||||||
if (fallback?.[1]) {
|
if (matched) return matched;
|
||||||
return { id: fallback[1], name: fallback[1] };
|
}
|
||||||
|
if (peers.length > 0) {
|
||||||
|
return peers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = result.combined.match(/Advertisement from\s+([^\s]+)/);
|
||||||
|
if (fallback?.[1]) {
|
||||||
|
return { id: fallback[1], name: fallback[1] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`No peers discovered\n${result.combined}`);
|
|
||||||
|
if (attempt < retries) {
|
||||||
|
const waitMs = 400 * (attempt + 1);
|
||||||
|
console.warn(
|
||||||
|
`[WARN] p2p-peers returned no usable peers, retrying (${attempt + 1}/${retries}) in ${waitMs}ms`
|
||||||
|
);
|
||||||
|
await sleep(waitMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
result.code !== 0 ? `p2p-peers failed\n${result.combined}` : `No peers discovered\n${result.combined}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return peers[0];
|
|
||||||
|
throw new Error(`No peers discovered\n${lastCombined}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function maybeStartLocalRelay(relay: string): Promise<boolean> {
|
export async function maybeStartLocalRelay(relay: string): Promise<boolean> {
|
||||||
if (!isLocalP2pRelay(relay)) return false;
|
if (!isLocalP2pRelay(relay)) return false;
|
||||||
await startP2pRelay();
|
await startP2pRelay();
|
||||||
|
const endpoint = parseRelayEndpoint(relay);
|
||||||
|
await waitForPort(endpoint.hostname, endpoint.port, {
|
||||||
|
timeoutMs: Number(Deno.env.get("LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS") ?? "15000"),
|
||||||
|
intervalMs: Number(Deno.env.get("LIVESYNC_P2P_RELAY_READY_INTERVAL_MS") ?? "250"),
|
||||||
|
connectTimeoutMs: Number(Deno.env.get("LIVESYNC_P2P_RELAY_CONNECT_TIMEOUT_MS") ?? "1000"),
|
||||||
|
});
|
||||||
|
// Docker proxy accepts TCP connections instantly before the container's internal process is fully ready.
|
||||||
|
// Wait an additional few seconds to ensure strfry is actually accepting WebSockets.
|
||||||
|
await sleep(3000);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +95,17 @@ export async function stopLocalRelayIfStarted(started: boolean): Promise<void> {
|
|||||||
await stopP2pRelay().catch(() => {});
|
await stopP2pRelay().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function maybeStartCoturn(turnServers: string): Promise<boolean> {
|
||||||
|
if (turnServers.includes("localhost") || turnServers.includes("127.0.0.1") || turnServers.includes("[::1]")) {
|
||||||
|
await startCoturn();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopCoturnIfStarted(started: boolean): Promise<void> {
|
||||||
|
if (started) {
|
||||||
|
await stopCoturn().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ export async function applyP2pSettings(
|
|||||||
passphrase: string,
|
passphrase: string,
|
||||||
appId = "self-hosted-livesync-cli-tests",
|
appId = "self-hosted-livesync-cli-tests",
|
||||||
relays = "ws://localhost:4000/",
|
relays = "ws://localhost:4000/",
|
||||||
autoAccept = "~.*"
|
autoAccept = "~.*",
|
||||||
|
turnServers = "turn:127.0.0.1:3478"
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const data = JSON.parse(await Deno.readTextFile(settingsFile));
|
const data = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||||
data.P2P_Enabled = true;
|
data.P2P_Enabled = true;
|
||||||
@@ -184,6 +185,9 @@ export async function applyP2pSettings(
|
|||||||
data.P2P_relays = relays;
|
data.P2P_relays = relays;
|
||||||
data.P2P_AutoAcceptingPeers = autoAccept;
|
data.P2P_AutoAcceptingPeers = autoAccept;
|
||||||
data.P2P_AutoDenyingPeers = "";
|
data.P2P_AutoDenyingPeers = "";
|
||||||
|
data.P2P_turnServers = turnServers;
|
||||||
|
data.P2P_turnUsername = "testuser";
|
||||||
|
data.P2P_turnCredential = "testpass";
|
||||||
data.P2P_IsHeadless = true;
|
data.P2P_IsHeadless = true;
|
||||||
data.isConfigured = true;
|
data.isConfigured = true;
|
||||||
await Deno.writeTextFile(settingsFile, JSON.stringify(data, null, 2));
|
await Deno.writeTextFile(settingsFile, JSON.stringify(data, null, 2));
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Deno port of test-daemon-linux.sh
|
||||||
|
*
|
||||||
|
* Tests daemon-related ignore rules behaviour.
|
||||||
|
*
|
||||||
|
* Tests that are runnable without a long-running daemon process are exercised
|
||||||
|
* here using the 'mirror' command, which calls the same 'isTargetFile' handler
|
||||||
|
* stack that the daemon uses.
|
||||||
|
*
|
||||||
|
* Covered cases:
|
||||||
|
* 1. .livesync/ignore with *.tmp pattern → ignored file is not synced to database
|
||||||
|
* 2. .livesync/ignore missing → no error, and normal synchronisation continues
|
||||||
|
* 3. import: .gitignore directive → patterns from .gitignore are merged
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* deno test -A test-daemon.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from "@std/path";
|
||||||
|
import { assertEquals } from "@std/assert";
|
||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { runCliOrFail, runCli, assertContains, assertNotContains } from "./helpers/cli.ts";
|
||||||
|
import { initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts";
|
||||||
|
|
||||||
|
Deno.test("daemon: ignore rules behaviour", async (t) => {
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Case 1: .livesync/ignore with *.tmp → ignored file not synced to database
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
await t.step("case 1: .livesync/ignore *.tmp prevents sync", async () => {
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-daemon-c1");
|
||||||
|
const settingsFile = workDir.join("data.json");
|
||||||
|
const vaultDir = workDir.join("vault");
|
||||||
|
|
||||||
|
await Deno.mkdir(join(vaultDir, ".livesync"), { recursive: true });
|
||||||
|
await Deno.mkdir(join(vaultDir, "notes"), { recursive: true });
|
||||||
|
|
||||||
|
await initSettingsFile(settingsFile);
|
||||||
|
await markSettingsConfigured(settingsFile);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(join(vaultDir, ".livesync", "ignore"), "*.tmp\n");
|
||||||
|
await Deno.writeTextFile(join(vaultDir, "notes", "normal.md"), "normal content\n");
|
||||||
|
await Deno.writeTextFile(join(vaultDir, "notes", "scratch.tmp"), "tmp content\n");
|
||||||
|
|
||||||
|
console.log("[INFO] Running mirror for Case 1...");
|
||||||
|
await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror");
|
||||||
|
|
||||||
|
// The normal file should be in the database.
|
||||||
|
const resultNormal = workDir.join("case1-normal.txt");
|
||||||
|
await runCliOrFail(vaultDir, "--settings", settingsFile, "pull", "notes/normal.md", resultNormal);
|
||||||
|
const normalContent = await Deno.readTextFile(resultNormal);
|
||||||
|
assertEquals(normalContent, "normal content\n", "normal.md content mismatch after mirror");
|
||||||
|
|
||||||
|
// The .tmp file should NOT be in the database.
|
||||||
|
const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls");
|
||||||
|
assertNotContains(dbList, "scratch.tmp", "scratch.tmp (ignored) was unexpectedly synced to database");
|
||||||
|
assertContains(dbList, "normal.md", "normal.md was not found in database after mirror");
|
||||||
|
console.log("[PASS] Case 1 verified successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Case 2: .livesync/ignore absent → no error, and normal synchronisation continues
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
await t.step("case 2: .livesync/ignore absent does not cause failure", async () => {
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-daemon-c2");
|
||||||
|
const settingsFile = workDir.join("data2.json");
|
||||||
|
const vaultDir = workDir.join("vault2");
|
||||||
|
|
||||||
|
await Deno.mkdir(join(vaultDir, "notes"), { recursive: true });
|
||||||
|
|
||||||
|
await initSettingsFile(settingsFile);
|
||||||
|
await markSettingsConfigured(settingsFile);
|
||||||
|
|
||||||
|
// No .livesync directory at all.
|
||||||
|
await Deno.writeTextFile(join(vaultDir, "notes", "hello.md"), "hello\n");
|
||||||
|
|
||||||
|
console.log("[INFO] Running mirror for Case 2...");
|
||||||
|
const result = await runCli(vaultDir, "--settings", settingsFile, "mirror");
|
||||||
|
assertEquals(result.code, 0, "mirror exited non-zero when .livesync/ignore is absent");
|
||||||
|
|
||||||
|
// The normal file should have been synced.
|
||||||
|
const resultHello = workDir.join("case2-hello.txt");
|
||||||
|
await runCliOrFail(vaultDir, "--settings", settingsFile, "pull", "notes/hello.md", resultHello);
|
||||||
|
const helloContent = await Deno.readTextFile(resultHello);
|
||||||
|
assertEquals(helloContent, "hello\n", "file content mismatch when .livesync/ignore is absent");
|
||||||
|
console.log("[PASS] Case 2 verified successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Case 3: import: .gitignore merges patterns
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
await t.step("case 3: import: .gitignore directive merges patterns", async () => {
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-daemon-c3");
|
||||||
|
const settingsFile = workDir.join("data3.json");
|
||||||
|
const vaultDir = workDir.join("vault3");
|
||||||
|
|
||||||
|
await Deno.mkdir(join(vaultDir, ".livesync"), { recursive: true });
|
||||||
|
await Deno.mkdir(join(vaultDir, "notes"), { recursive: true });
|
||||||
|
|
||||||
|
await initSettingsFile(settingsFile);
|
||||||
|
await markSettingsConfigured(settingsFile);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(join(vaultDir, ".livesync", "ignore"), "import: .gitignore\n");
|
||||||
|
await Deno.writeTextFile(join(vaultDir, ".gitignore"), "# gitignore comment\n*.log\nbuild/\n");
|
||||||
|
|
||||||
|
await Deno.writeTextFile(join(vaultDir, "notes", "regular.md"), "regular note\n");
|
||||||
|
await Deno.writeTextFile(join(vaultDir, "notes", "debug.log"), "log content\n");
|
||||||
|
|
||||||
|
console.log("[INFO] Running mirror for Case 3...");
|
||||||
|
await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror");
|
||||||
|
|
||||||
|
const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls");
|
||||||
|
assertNotContains(
|
||||||
|
dbList,
|
||||||
|
"debug.log",
|
||||||
|
"debug.log (ignored via .gitignore import) was unexpectedly synced to database"
|
||||||
|
);
|
||||||
|
assertContains(dbList, "regular.md", "regular.md was not synced normally alongside .gitignore import rules");
|
||||||
|
console.log("[PASS] Case 3 verified successfully");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Deno port of test-decoupled-vault-linux.sh
|
||||||
|
*
|
||||||
|
* Tests push, pull, and mirror command behaviour when the vault directory is
|
||||||
|
* decoupled (separated) from the database directory.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* deno test -A test-decoupled-vault.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from "@std/path";
|
||||||
|
import { assertEquals } from "@std/assert";
|
||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { runCliOrFail } from "./helpers/cli.ts";
|
||||||
|
import { applyCouchdbSettings, initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts";
|
||||||
|
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
|
||||||
|
|
||||||
|
const REMOTE_PATH = Deno.env.get("REMOTE_PATH") ?? "test/push-pull-decoupled.txt";
|
||||||
|
|
||||||
|
Deno.test("decoupled database and vault", async () => {
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-decoupled");
|
||||||
|
|
||||||
|
const settingsFile = workDir.join("data.json");
|
||||||
|
const vaultDir = workDir.join("vault");
|
||||||
|
const dbDir = workDir.join("db");
|
||||||
|
|
||||||
|
await Deno.mkdir(join(vaultDir, "test"), { recursive: true });
|
||||||
|
await Deno.mkdir(dbDir, { recursive: true });
|
||||||
|
|
||||||
|
const uri = Deno.env.get("COUCHDB_URI") ?? "http://127.0.0.1:5989/";
|
||||||
|
const user = Deno.env.get("COUCHDB_USER") ?? "admin";
|
||||||
|
const password = Deno.env.get("COUCHDB_PASSWORD") ?? "testpassword";
|
||||||
|
const dbname = Deno.env.get("COUCHDB_DBNAME") ?? `decoupled-${Date.now()}`;
|
||||||
|
|
||||||
|
const shouldStartDocker = Deno.env.get("LIVESYNC_START_DOCKER") !== "0";
|
||||||
|
const keepDocker = Deno.env.get("LIVESYNC_DEBUG_KEEP_DOCKER") === "1";
|
||||||
|
|
||||||
|
if (shouldStartDocker) {
|
||||||
|
await startCouchdb(uri, user, password, dbname);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initSettingsFile(settingsFile);
|
||||||
|
|
||||||
|
if (uri && user && password && dbname) {
|
||||||
|
console.log("[INFO] applying CouchDB environment variables to settings");
|
||||||
|
await applyCouchdbSettings(settingsFile, uri, user, password, dbname);
|
||||||
|
} else {
|
||||||
|
console.warn("[WARN] CouchDB environment variables are not fully set. Push and pull operations may fail.");
|
||||||
|
await markSettingsConfigured(settingsFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcFile = workDir.join("push-source.txt");
|
||||||
|
const pulledFile = workDir.join("pull-result.txt");
|
||||||
|
const content = `push-pull-decoupled-test ${new Date().toISOString()}\n`;
|
||||||
|
await Deno.writeTextFile(srcFile, content);
|
||||||
|
|
||||||
|
// 1. Test push command with decoupled vault directory
|
||||||
|
console.log(`[INFO] push with decoupled vault -> ${REMOTE_PATH}`);
|
||||||
|
await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "push", srcFile, REMOTE_PATH);
|
||||||
|
|
||||||
|
// 2. Test pull command with decoupled vault directory
|
||||||
|
console.log(`[INFO] pull with decoupled vault <- ${REMOTE_PATH}`);
|
||||||
|
await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "pull", REMOTE_PATH, pulledFile);
|
||||||
|
|
||||||
|
const pulled = await Deno.readTextFile(pulledFile);
|
||||||
|
assertEquals(pulled, content, "push/pull roundtrip with decoupled vault content mismatch");
|
||||||
|
console.log("[PASS] push/pull roundtrip with decoupled vault matched");
|
||||||
|
|
||||||
|
// 3. Clean up pulled file and vault test directory to verify mirror
|
||||||
|
await Deno.remove(pulledFile).catch(() => {});
|
||||||
|
await Deno.remove(join(vaultDir, "test"), { recursive: true }).catch(() => {});
|
||||||
|
|
||||||
|
// 4. Test mirror command with decoupled vault directory
|
||||||
|
console.log("[INFO] mirror with decoupled vault");
|
||||||
|
await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "mirror");
|
||||||
|
|
||||||
|
const restoredFile = join(vaultDir, REMOTE_PATH);
|
||||||
|
const restored = await Deno.readTextFile(restoredFile);
|
||||||
|
assertEquals(restored, content, "mirror with decoupled vault content mismatch");
|
||||||
|
console.log("[PASS] mirror with decoupled vault matched");
|
||||||
|
} finally {
|
||||||
|
if (shouldStartDocker && !keepDocker) {
|
||||||
|
await stopCouchdb().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -39,6 +39,10 @@ Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
|||||||
// isConfigured=true is required for canProceedScan in the mirror command.
|
// isConfigured=true is required for canProceedScan in the mirror command.
|
||||||
await markSettingsConfigured(settingsFile);
|
await markSettingsConfigured(settingsFile);
|
||||||
|
|
||||||
|
const data = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||||
|
data.writeDocumentsIfConflicted = true;
|
||||||
|
await Deno.writeTextFile(settingsFile, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
// Copy settings to the DB directory (separated-path mode)
|
// Copy settings to the DB directory (separated-path mode)
|
||||||
const dbSettings = workDir.join("db", "settings.json");
|
const dbSettings = workDir.join("db", "settings.json");
|
||||||
await Deno.copyFile(settingsFile, dbSettings);
|
await Deno.copyFile(settingsFile, dbSettings);
|
||||||
|
|||||||
@@ -2,13 +2,28 @@ import { assert } from "@std/assert";
|
|||||||
import { TempDir } from "./helpers/temp.ts";
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
import { initSettingsFile, applyP2pSettings, applyP2pTestTweaks } from "./helpers/settings.ts";
|
import { initSettingsFile, applyP2pSettings, applyP2pTestTweaks } from "./helpers/settings.ts";
|
||||||
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
||||||
import { discoverPeer, maybeStartLocalRelay, stopLocalRelayIfStarted } from "./helpers/p2p.ts";
|
import {
|
||||||
|
discoverPeer,
|
||||||
|
maybeStartLocalRelay,
|
||||||
|
stopLocalRelayIfStarted,
|
||||||
|
maybeStartCoturn,
|
||||||
|
stopCoturnIfStarted,
|
||||||
|
} from "./helpers/p2p.ts";
|
||||||
|
import { getOptimalLoopbackIp } from "./helpers/net.ts";
|
||||||
|
|
||||||
Deno.test("p2p-peers: discovers host through local relay", async () => {
|
Deno.test("p2p-peers: discovers host through local relay", async () => {
|
||||||
const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/";
|
const loopbackIp = await getOptimalLoopbackIp();
|
||||||
|
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
|
||||||
|
|
||||||
|
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
|
||||||
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
|
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
|
||||||
const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
|
const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
|
||||||
const timeoutSeconds = Number(Deno.env.get("TIMEOUT_SECONDS") ?? "8");
|
const timeoutSeconds = Number(Deno.env.get("TIMEOUT_SECONDS") ?? "8");
|
||||||
|
const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
|
const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`;
|
||||||
|
const clientPeerName = Deno.env.get("CLIENT_PEER_NAME") ?? `p2p-client-${nonce}`;
|
||||||
|
const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0";
|
||||||
|
const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none");
|
||||||
|
|
||||||
await using workDir = await TempDir.create("livesync-cli-p2p-peers-local-relay");
|
await using workDir = await TempDir.create("livesync-cli-p2p-peers-local-relay");
|
||||||
const hostVault = workDir.join("vault-host");
|
const hostVault = workDir.join("vault-host");
|
||||||
@@ -19,24 +34,43 @@ Deno.test("p2p-peers: discovers host through local relay", async () => {
|
|||||||
await Deno.mkdir(clientVault, { recursive: true });
|
await Deno.mkdir(clientVault, { recursive: true });
|
||||||
|
|
||||||
const relayStarted = await maybeStartLocalRelay(relay);
|
const relayStarted = await maybeStartLocalRelay(relay);
|
||||||
|
const coturnStarted = await maybeStartCoturn(turnServers);
|
||||||
try {
|
try {
|
||||||
await initSettingsFile(hostSettings);
|
await initSettingsFile(hostSettings);
|
||||||
await initSettingsFile(clientSettings);
|
await initSettingsFile(clientSettings);
|
||||||
await applyP2pSettings(hostSettings, roomId, passphrase, "self-hosted-livesync-cli-tests", relay);
|
await applyP2pSettings(
|
||||||
await applyP2pSettings(clientSettings, roomId, passphrase, "self-hosted-livesync-cli-tests", relay);
|
hostSettings,
|
||||||
await applyP2pTestTweaks(hostSettings, "p2p-host", passphrase);
|
roomId,
|
||||||
await applyP2pTestTweaks(clientSettings, "p2p-client", passphrase);
|
passphrase,
|
||||||
|
"self-hosted-livesync-cli-tests",
|
||||||
|
relay,
|
||||||
|
"~.*",
|
||||||
|
turnServers
|
||||||
|
);
|
||||||
|
await applyP2pSettings(
|
||||||
|
clientSettings,
|
||||||
|
roomId,
|
||||||
|
passphrase,
|
||||||
|
"self-hosted-livesync-cli-tests",
|
||||||
|
relay,
|
||||||
|
"~.*",
|
||||||
|
turnServers
|
||||||
|
);
|
||||||
|
await applyP2pTestTweaks(hostSettings, hostPeerName, passphrase);
|
||||||
|
await applyP2pTestTweaks(clientSettings, clientPeerName, passphrase);
|
||||||
|
|
||||||
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
||||||
try {
|
try {
|
||||||
await host.waitUntilContains("P2P host is running", 20000);
|
await host.waitUntilContains("P2P host is running", 20000);
|
||||||
const peer = await discoverPeer(clientVault, clientSettings, timeoutSeconds);
|
const peer = await discoverPeer(clientVault, clientSettings, timeoutSeconds, hostPeerName);
|
||||||
assert(peer.id.length > 0);
|
assert(peer.id.length > 0);
|
||||||
assert(peer.name.length > 0);
|
assert(peer.name.length > 0);
|
||||||
|
assert(peer.name === hostPeerName, `expected peer '${hostPeerName}', got '${peer.name}'`);
|
||||||
} finally {
|
} finally {
|
||||||
await host.stop();
|
await host.stop();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await stopLocalRelayIfStarted(relayStarted);
|
await stopLocalRelayIfStarted(relayStarted);
|
||||||
|
await stopCoturnIfStarted(coturnStarted);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,30 @@ import { assert } from "@std/assert";
|
|||||||
import { TempDir } from "./helpers/temp.ts";
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
import { initSettingsFile, applyP2pSettings, applyP2pTestTweaks } from "./helpers/settings.ts";
|
import { initSettingsFile, applyP2pSettings, applyP2pTestTweaks } from "./helpers/settings.ts";
|
||||||
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
||||||
import { discoverPeer, maybeStartLocalRelay, stopLocalRelayIfStarted } from "./helpers/p2p.ts";
|
import {
|
||||||
|
discoverPeer,
|
||||||
|
maybeStartLocalRelay,
|
||||||
|
stopLocalRelayIfStarted,
|
||||||
|
maybeStartCoturn,
|
||||||
|
stopCoturnIfStarted,
|
||||||
|
} from "./helpers/p2p.ts";
|
||||||
import { runCli } from "./helpers/cli.ts";
|
import { runCli } from "./helpers/cli.ts";
|
||||||
|
import { getOptimalLoopbackIp } from "./helpers/net.ts";
|
||||||
|
|
||||||
Deno.test("p2p-sync: discovers peer and completes sync", async () => {
|
Deno.test("p2p-sync: discovers peer and completes sync", async () => {
|
||||||
const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/";
|
const loopbackIp = await getOptimalLoopbackIp();
|
||||||
|
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
|
||||||
|
|
||||||
|
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
|
||||||
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
|
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
|
||||||
const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
|
const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
|
||||||
const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "12");
|
const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "12");
|
||||||
const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "15");
|
const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "15");
|
||||||
|
const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
|
const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`;
|
||||||
|
const clientPeerName = Deno.env.get("CLIENT_PEER_NAME") ?? `p2p-client-${nonce}`;
|
||||||
|
const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0";
|
||||||
|
const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none");
|
||||||
|
|
||||||
await using workDir = await TempDir.create("livesync-cli-p2p-sync");
|
await using workDir = await TempDir.create("livesync-cli-p2p-sync");
|
||||||
const hostVault = workDir.join("vault-host");
|
const hostVault = workDir.join("vault-host");
|
||||||
@@ -21,13 +36,30 @@ Deno.test("p2p-sync: discovers peer and completes sync", async () => {
|
|||||||
await Deno.mkdir(clientVault, { recursive: true });
|
await Deno.mkdir(clientVault, { recursive: true });
|
||||||
|
|
||||||
const relayStarted = await maybeStartLocalRelay(relay);
|
const relayStarted = await maybeStartLocalRelay(relay);
|
||||||
|
const coturnStarted = await maybeStartCoturn(turnServers);
|
||||||
try {
|
try {
|
||||||
await initSettingsFile(hostSettings);
|
await initSettingsFile(hostSettings);
|
||||||
await initSettingsFile(clientSettings);
|
await initSettingsFile(clientSettings);
|
||||||
await applyP2pSettings(hostSettings, roomId, passphrase, "self-hosted-livesync-cli-tests", relay);
|
await applyP2pSettings(
|
||||||
await applyP2pSettings(clientSettings, roomId, passphrase, "self-hosted-livesync-cli-tests", relay);
|
hostSettings,
|
||||||
await applyP2pTestTweaks(hostSettings, "p2p-host", passphrase);
|
roomId,
|
||||||
await applyP2pTestTweaks(clientSettings, "p2p-client", passphrase);
|
passphrase,
|
||||||
|
"self-hosted-livesync-cli-tests",
|
||||||
|
relay,
|
||||||
|
"~.*",
|
||||||
|
turnServers
|
||||||
|
);
|
||||||
|
await applyP2pSettings(
|
||||||
|
clientSettings,
|
||||||
|
roomId,
|
||||||
|
passphrase,
|
||||||
|
"self-hosted-livesync-cli-tests",
|
||||||
|
relay,
|
||||||
|
"~.*",
|
||||||
|
turnServers
|
||||||
|
);
|
||||||
|
await applyP2pTestTweaks(hostSettings, hostPeerName, passphrase);
|
||||||
|
await applyP2pTestTweaks(clientSettings, clientPeerName, passphrase);
|
||||||
|
|
||||||
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
||||||
try {
|
try {
|
||||||
@@ -36,7 +68,7 @@ Deno.test("p2p-sync: discovers peer and completes sync", async () => {
|
|||||||
clientVault,
|
clientVault,
|
||||||
clientSettings,
|
clientSettings,
|
||||||
peersTimeout,
|
peersTimeout,
|
||||||
Deno.env.get("TARGET_PEER") ?? undefined
|
Deno.env.get("TARGET_PEER") ?? hostPeerName
|
||||||
);
|
);
|
||||||
const syncResult = await runCli(
|
const syncResult = await runCli(
|
||||||
clientVault,
|
clientVault,
|
||||||
@@ -55,5 +87,6 @@ Deno.test("p2p-sync: discovers peer and completes sync", async () => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await stopLocalRelayIfStarted(relayStarted);
|
await stopLocalRelayIfStarted(relayStarted);
|
||||||
|
await stopCoturnIfStarted(coturnStarted);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import { assert } from "@std/assert";
|
import { assert } from "@std/assert";
|
||||||
import { TempDir } from "./helpers/temp.ts";
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
import { applyP2pSettings, initSettingsFile } from "./helpers/settings.ts";
|
import { applyP2pSettings, applyP2pTestTweaks, initSettingsFile } from "./helpers/settings.ts";
|
||||||
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
||||||
import { discoverPeer, maybeStartLocalRelay, stopLocalRelayIfStarted } from "./helpers/p2p.ts";
|
import {
|
||||||
|
discoverPeer,
|
||||||
|
maybeStartLocalRelay,
|
||||||
|
stopLocalRelayIfStarted,
|
||||||
|
maybeStartCoturn,
|
||||||
|
stopCoturnIfStarted,
|
||||||
|
} from "./helpers/p2p.ts";
|
||||||
import { jsonStringField, runCliOrFail, runCliWithInputOrFail, sanitiseCatStdout } from "./helpers/cli.ts";
|
import { jsonStringField, runCliOrFail, runCliWithInputOrFail, sanitiseCatStdout } from "./helpers/cli.ts";
|
||||||
|
import { getOptimalLoopbackIp } from "./helpers/net.ts";
|
||||||
|
|
||||||
Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
|
Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
|
||||||
const relay = Deno.env.get("RELAY") ?? "ws://localhost:4000/";
|
const loopbackIp = await getOptimalLoopbackIp();
|
||||||
const roomId = `${Deno.env.get("ROOM_ID_PREFIX") ?? "p2p-room"}-${Date.now()}`;
|
const loopbackHost = loopbackIp === "::1" ? "[::1]" : loopbackIp;
|
||||||
const passphrase = `${Deno.env.get("PASSPHRASE_PREFIX") ?? "p2p-pass"}-${Date.now()}`;
|
|
||||||
const appId = Deno.env.get("APP_ID") ?? "self-hosted-livesync-cli-tests";
|
const relay = Deno.env.get("RELAY") ?? `ws://${loopbackHost}:4000/`;
|
||||||
|
const roomId = Deno.env.get("ROOM_ID") ?? `room-${Date.now()}`;
|
||||||
|
const passphrase = Deno.env.get("PASSPHRASE") ?? "test";
|
||||||
|
const appId = "self-hosted-livesync-cli-tests";
|
||||||
const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "10");
|
const peersTimeout = Number(Deno.env.get("PEERS_TIMEOUT") ?? "10");
|
||||||
const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "15");
|
const syncTimeout = Number(Deno.env.get("SYNC_TIMEOUT") ?? "15");
|
||||||
|
const nonce = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
|
const hostPeerName = Deno.env.get("HOST_PEER_NAME") ?? `p2p-host-${nonce}`;
|
||||||
|
const peerNameB = Deno.env.get("PEER_NAME_B") ?? `p2p-client-b-${nonce}`;
|
||||||
|
const peerNameC = Deno.env.get("PEER_NAME_C") ?? `p2p-client-c-${nonce}`;
|
||||||
|
const useCoturn = Deno.env.get("LIVESYNC_USE_COTURN") !== "0";
|
||||||
|
const turnServers = Deno.env.get("TURN_SERVERS") ?? (useCoturn ? `turn:${loopbackHost}:3478` : "none");
|
||||||
|
|
||||||
await using workDir = await TempDir.create("livesync-cli-p2p-3nodes");
|
await using workDir = await TempDir.create("livesync-cli-p2p-3nodes");
|
||||||
const vaultA = workDir.join("vault-a");
|
const vaultA = workDir.join("vault-a");
|
||||||
@@ -25,17 +41,23 @@ Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
|
|||||||
await Deno.mkdir(vaultC, { recursive: true });
|
await Deno.mkdir(vaultC, { recursive: true });
|
||||||
|
|
||||||
const relayStarted = await maybeStartLocalRelay(relay);
|
const relayStarted = await maybeStartLocalRelay(relay);
|
||||||
|
const coturnStarted = await maybeStartCoturn(turnServers);
|
||||||
try {
|
try {
|
||||||
for (const settings of [settingsA, settingsB, settingsC]) {
|
await initSettingsFile(settingsA);
|
||||||
await initSettingsFile(settings);
|
await initSettingsFile(settingsB);
|
||||||
await applyP2pSettings(settings, roomId, passphrase, appId, relay);
|
await initSettingsFile(settingsC);
|
||||||
}
|
await applyP2pSettings(settingsA, roomId, passphrase, appId, relay, "~.*", turnServers);
|
||||||
|
await applyP2pSettings(settingsB, roomId, passphrase, appId, relay, "~.*", turnServers);
|
||||||
|
await applyP2pSettings(settingsC, roomId, passphrase, appId, relay, "~.*", turnServers);
|
||||||
|
await applyP2pTestTweaks(settingsA, hostPeerName, passphrase);
|
||||||
|
await applyP2pTestTweaks(settingsB, peerNameB, passphrase);
|
||||||
|
await applyP2pTestTweaks(settingsC, peerNameC, passphrase);
|
||||||
|
|
||||||
const host = startCliInBackground(vaultA, "--settings", settingsA, "p2p-host");
|
const host = startCliInBackground(vaultA, "--settings", settingsA, "p2p-host");
|
||||||
try {
|
try {
|
||||||
await host.waitUntilContains("P2P host is running", 20000);
|
await host.waitUntilContains("P2P host is running", 20000);
|
||||||
const peerFromB = await discoverPeer(vaultB, settingsB, peersTimeout);
|
const peerFromB = await discoverPeer(vaultB, settingsB, peersTimeout, hostPeerName);
|
||||||
const peerFromC = await discoverPeer(vaultC, settingsC, peersTimeout);
|
const peerFromC = await discoverPeer(vaultC, settingsC, peersTimeout, hostPeerName);
|
||||||
const targetPath = "p2p/conflicted-from-two-clients.txt";
|
const targetPath = "p2p/conflicted-from-two-clients.txt";
|
||||||
|
|
||||||
await runCliWithInputOrFail("from-client-b-v1\n", vaultB, "--settings", settingsB, "put", targetPath);
|
await runCliWithInputOrFail("from-client-b-v1\n", vaultB, "--settings", settingsB, "put", targetPath);
|
||||||
@@ -114,5 +136,6 @@ Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await stopLocalRelayIfStarted(relayStarted);
|
await stopLocalRelayIfStarted(relayStarted);
|
||||||
|
await stopCoturnIfStarted(coturnStarted);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Deno port of test-remote-commands-linux.sh
|
||||||
|
*
|
||||||
|
* Tests remote management commands: remote-status, lock-remote, unlock-remote,
|
||||||
|
* and mark-resolved.
|
||||||
|
*
|
||||||
|
* Scenario:
|
||||||
|
* 1. Start CouchDB, create a test database, and perform an initial sync.
|
||||||
|
* 2. Run remote-status and assert that the output contains the database name in JSON format.
|
||||||
|
* 3. Run lock-remote and verify that the remote database is locked.
|
||||||
|
* 4. Lock the remote database milestone manually, verify status, and run unlock-remote.
|
||||||
|
* Assert that the output of unlock-remote contains the unlocked verification status.
|
||||||
|
* 5. Lock the remote database milestone manually, run mark-resolved, and verify that the
|
||||||
|
* current device is accepted.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* deno test -A test-remote-commands.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from "@std/path";
|
||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { runCli, assertContains } from "./helpers/cli.ts";
|
||||||
|
import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts";
|
||||||
|
import { startCouchdb, stopCouchdb, updateCouchdbDoc } from "./helpers/docker.ts";
|
||||||
|
|
||||||
|
async function runCliCombinedOrFail(...args: string[]): Promise<string> {
|
||||||
|
const res = await runCli(...args);
|
||||||
|
if (res.code !== 0) {
|
||||||
|
throw new Error(`CLI exited with code ${res.code}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`);
|
||||||
|
}
|
||||||
|
return res.combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("remote management commands", async () => {
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-remote-cmds");
|
||||||
|
|
||||||
|
const settingsFile = workDir.join("settings.json");
|
||||||
|
const vaultDir = workDir.join("vault");
|
||||||
|
await Deno.mkdir(vaultDir, { recursive: true });
|
||||||
|
|
||||||
|
const uri = Deno.env.get("COUCHDB_URI") ?? "http://127.0.0.1:5989/";
|
||||||
|
const user = Deno.env.get("COUCHDB_USER") ?? "admin";
|
||||||
|
const password = Deno.env.get("COUCHDB_PASSWORD") ?? "testpassword";
|
||||||
|
const dbSuffix = `${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||||
|
const dbname = Deno.env.get("COUCHDB_DBNAME") ?? `remotes-${dbSuffix}`;
|
||||||
|
|
||||||
|
const shouldStartDocker = Deno.env.get("LIVESYNC_START_DOCKER") !== "0";
|
||||||
|
const keepDocker = Deno.env.get("LIVESYNC_DEBUG_KEEP_DOCKER") === "1";
|
||||||
|
|
||||||
|
if (shouldStartDocker) {
|
||||||
|
await startCouchdb(uri, user, password, dbname);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initSettingsFile(settingsFile);
|
||||||
|
await applyCouchdbSettings(settingsFile, uri, user, password, dbname, true);
|
||||||
|
|
||||||
|
console.log("[INFO] Performing initial sync to create milestone document...");
|
||||||
|
await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "sync");
|
||||||
|
|
||||||
|
// 1. remote-status outputs valid JSON with CouchDB details
|
||||||
|
console.log("[CASE] remote-status outputs valid JSON with CouchDB details");
|
||||||
|
const statusOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "remote-status");
|
||||||
|
assertContains(statusOutput, `"db_name": "${dbname}"`, "remote-status should return JSON containing db_name");
|
||||||
|
console.log("[PASS] remote-status verified");
|
||||||
|
|
||||||
|
// 2. lock-remote locks and verifies state
|
||||||
|
console.log("[CASE] lock-remote locks and verifies state");
|
||||||
|
const lockOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "lock-remote");
|
||||||
|
assertContains(
|
||||||
|
lockOutput,
|
||||||
|
"[Verification] Remote Database: LOCKED",
|
||||||
|
"lock-remote output should show that the remote database is locked"
|
||||||
|
);
|
||||||
|
console.log("[PASS] lock-remote verified");
|
||||||
|
|
||||||
|
// 3. unlock-remote unlocks and verifies state
|
||||||
|
console.log("[CASE] unlock-remote unlocks and verifies state");
|
||||||
|
// Manually lock milestone
|
||||||
|
console.log("[INFO] Manually locking milestone...");
|
||||||
|
await updateCouchdbDoc(uri, user, password, `${dbname}/_local/obsydian_livesync_milestone`, (doc) => {
|
||||||
|
doc.locked = true;
|
||||||
|
doc.accepted_nodes = [];
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run unlock-remote and verify output contains verification message
|
||||||
|
const unlockOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "unlock-remote");
|
||||||
|
assertContains(
|
||||||
|
unlockOutput,
|
||||||
|
"[Verification] Remote Database: UNLOCKED",
|
||||||
|
"unlock-remote output should contain verification status"
|
||||||
|
);
|
||||||
|
console.log("[PASS] unlock-remote verified");
|
||||||
|
|
||||||
|
// 4. mark-resolved resolves and verifies state
|
||||||
|
console.log("[CASE] mark-resolved resolves and verifies state");
|
||||||
|
// Manually lock milestone
|
||||||
|
console.log("[INFO] Manually locking milestone...");
|
||||||
|
await updateCouchdbDoc(uri, user, password, `${dbname}/_local/obsydian_livesync_milestone`, (doc) => {
|
||||||
|
doc.locked = true;
|
||||||
|
doc.accepted_nodes = [];
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run mark-resolved and verify output contains verification messages
|
||||||
|
const resolvedOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "mark-resolved");
|
||||||
|
assertContains(
|
||||||
|
resolvedOutput,
|
||||||
|
"[Verification] Remote Database: LOCKED",
|
||||||
|
"mark-resolved output should show that the remote database remains locked"
|
||||||
|
);
|
||||||
|
assertContains(
|
||||||
|
resolvedOutput,
|
||||||
|
"ACCEPTED",
|
||||||
|
"mark-resolved output should show that the current device node is accepted"
|
||||||
|
);
|
||||||
|
console.log("[PASS] mark-resolved verified");
|
||||||
|
|
||||||
|
console.log("[ALL PASS] All remote CLI commands verified successfully");
|
||||||
|
} finally {
|
||||||
|
if (shouldStartDocker && !keepDocker) {
|
||||||
|
await stopCouchdb().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -132,7 +132,7 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
|||||||
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
|
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
|
||||||
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
|
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
|
||||||
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
|
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
|
||||||
assert(typeof data.chunks === "number" && (data.chunks as number) >= 1, `info .chunks invalid: ${data.chunks}`);
|
assert(typeof data.chunks === "number" && (data.chunks) >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||||
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
|
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
|
||||||
console.log("[PASS] info output format matched");
|
console.log("[PASS] info output format matched");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ src/apps/cli/testdeno/
|
|||||||
test-mirror.ts
|
test-mirror.ts
|
||||||
test-sync-two-local-databases.ts
|
test-sync-two-local-databases.ts
|
||||||
test-sync-locked-remote.ts
|
test-sync-locked-remote.ts
|
||||||
|
test-daemon.ts
|
||||||
|
test-decoupled-vault.ts
|
||||||
|
test-remote-commands.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -54,6 +57,9 @@ Main tasks:
|
|||||||
|
|
||||||
- `deno task test`
|
- `deno task test`
|
||||||
- `deno task test:local`
|
- `deno task test:local`
|
||||||
|
- `deno task test:daemon`
|
||||||
|
- `deno task test:decoupled-vault`
|
||||||
|
- `deno task test:remote-commands`
|
||||||
- `deno task test:push-pull`
|
- `deno task test:push-pull`
|
||||||
- `deno task test:setup-put-cat`
|
- `deno task test:setup-put-cat`
|
||||||
- `deno task test:mirror`
|
- `deno task test:mirror`
|
||||||
@@ -183,6 +189,19 @@ Both CouchDB and P2P relay flows are bash-independent.
|
|||||||
- `MINIO-enc0`
|
- `MINIO-enc0`
|
||||||
- `MINIO-enc1`
|
- `MINIO-enc1`
|
||||||
|
|
||||||
|
### `test-daemon.ts`
|
||||||
|
|
||||||
|
- Verifies daemon-related ignore rules behaviour.
|
||||||
|
- Exercises scenarios with `.livesync/ignore` wildcard rules, missing ignore rules, and imported `.gitignore` rules.
|
||||||
|
|
||||||
|
### `test-decoupled-vault.ts`
|
||||||
|
|
||||||
|
- Verifies push, pull, and mirror command behaviour when the vault directory is decoupled from the database directory.
|
||||||
|
|
||||||
|
### `test-remote-commands.ts`
|
||||||
|
|
||||||
|
- Verifies remote database management commands: `remote-status`, `lock-remote`, `unlock-remote`, and `mark-resolved`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Running tests (PowerShell)
|
## Running tests (PowerShell)
|
||||||
@@ -198,11 +217,14 @@ deno task test:local
|
|||||||
# Individual tests
|
# Individual tests
|
||||||
deno task test:setup-put-cat
|
deno task test:setup-put-cat
|
||||||
deno task test:mirror
|
deno task test:mirror
|
||||||
|
deno task test:daemon
|
||||||
deno task test:push-pull
|
deno task test:push-pull
|
||||||
deno task test:sync-locked-remote
|
deno task test:sync-locked-remote
|
||||||
|
|
||||||
# CouchDB-based tests
|
# CouchDB-based tests
|
||||||
deno task test:sync-two-local
|
deno task test:sync-two-local
|
||||||
|
deno task test:decoupled-vault
|
||||||
|
deno task test:remote-commands
|
||||||
deno task test:e2e-couchdb
|
deno task test:e2e-couchdb
|
||||||
|
|
||||||
# P2P-based tests
|
# P2P-based tests
|
||||||
@@ -281,7 +303,7 @@ deno task test:sync-two-local
|
|||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|
||||||
The GitHub Actions workflow `.github/workflows/cli-deno-tests.yml` is used to run these tests automatically on push and pull requests affecting the CLI.
|
The GitHub Actions workflow `.github/workflows/cli-deno-tests.yml` runs automatically on pushes and pull requests affecting the CLI, executing the non-P2P test suite (`test:ci`). P2P tests (`test:p2p`) are excluded from automatic execution and must be run via manual dispatch (`workflow_dispatch`). You can optionally check the "Enable verbose and debug logging" checkbox during a manual dispatch to produce detailed trace logs for troubleshooting.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,18 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": false,
|
"strict": true,
|
||||||
|
// "noImplicitAny": false,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
// "rootDir": "../../../",
|
||||||
/* Path mapping */
|
/* Path mapping */
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["../../*"],
|
"@/*": ["../../*"],
|
||||||
"@lib/*": ["../../lib/src/*"]
|
"@lib/*": ["../../lib/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist", "test", "testdeno"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,15 @@ npm install
|
|||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev -w livesync-webapp
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from the package directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the project (ensure you are in `src/apps/webapp` directory)
|
|
||||||
cd src/apps/webapp
|
cd src/apps/webapp
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
@@ -45,8 +52,15 @@ This will start a development server at `http://localhost:3000`.
|
|||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build -w livesync-webapp
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from the package directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the project (ensure you are in `src/apps/webapp` directory)
|
|
||||||
cd src/apps/webapp
|
cd src/apps/webapp
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { LiveSyncWebApp } from "./main";
|
import { LiveSyncWebApp } from "./main";
|
||||||
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
|
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
|
||||||
|
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
const historyStore = new VaultHistoryStore();
|
const historyStore = new VaultHistoryStore();
|
||||||
let app: LiveSyncWebApp | null = null;
|
let app: LiveSyncWebApp | null = null;
|
||||||
|
|
||||||
function getRequiredElement<T extends HTMLElement>(id: string): T {
|
function getRequiredElement<T extends HTMLElement>(id: string): T {
|
||||||
const element = document.getElementById(id);
|
const element = _activeDocument.getElementById(id);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error(`Missing element: #${id}`);
|
throw new Error(`Missing element: #${id}`);
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ function setBusyState(isBusy: boolean): void {
|
|||||||
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
|
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
|
||||||
pickNewBtn.disabled = isBusy;
|
pickNewBtn.disabled = isBusy;
|
||||||
|
|
||||||
const historyButtons = document.querySelectorAll<HTMLButtonElement>(".vault-item button");
|
const historyButtons = _activeDocument.querySelectorAll<HTMLButtonElement>(".vault-item button");
|
||||||
historyButtons.forEach((button) => {
|
historyButtons.forEach((button) => {
|
||||||
button.disabled = isBusy;
|
button.disabled = isBusy;
|
||||||
});
|
});
|
||||||
@@ -45,24 +46,24 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
|
|||||||
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const row = document.createElement("div");
|
const row = _activeDocument.createElement("div");
|
||||||
row.className = "vault-item";
|
row.className = "vault-item";
|
||||||
|
|
||||||
const info = document.createElement("div");
|
const info = _activeDocument.createElement("div");
|
||||||
info.className = "vault-item-info";
|
info.className = "vault-item-info";
|
||||||
|
|
||||||
const name = document.createElement("div");
|
const name = _activeDocument.createElement("div");
|
||||||
name.className = "vault-item-name";
|
name.className = "vault-item-name";
|
||||||
name.textContent = item.name;
|
name.textContent = item.name;
|
||||||
|
|
||||||
const meta = document.createElement("div");
|
const meta = _activeDocument.createElement("div");
|
||||||
meta.className = "vault-item-meta";
|
meta.className = "vault-item-meta";
|
||||||
const label = item.id === lastUsedId ? "Last used" : "Used";
|
const label = item.id === lastUsedId ? "Last used" : "Used";
|
||||||
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
|
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
|
||||||
|
|
||||||
info.append(name, meta);
|
info.append(name, meta);
|
||||||
|
|
||||||
const useButton = document.createElement("button");
|
const useButton = _activeDocument.createElement("button");
|
||||||
useButton.type = "button";
|
useButton.type = "button";
|
||||||
useButton.textContent = "Use this vault";
|
useButton.textContent = "Use this vault";
|
||||||
useButton.addEventListener("click", () => {
|
useButton.addEventListener("click", () => {
|
||||||
@@ -120,7 +121,7 @@ async function initializeVaultSelector(): Promise<void> {
|
|||||||
await renderHistoryList();
|
await renderHistoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", async () => {
|
compatGlobal.addEventListener("load", async () => {
|
||||||
try {
|
try {
|
||||||
await initializeVaultSelector();
|
await initializeVaultSelector();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -129,11 +130,11 @@ window.addEventListener("load", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
compatGlobal.addEventListener("beforeunload", () => {
|
||||||
void app?.shutdown();
|
void app?.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
(window as any).livesyncApp = {
|
(compatGlobal as any).livesyncApp = {
|
||||||
getApp: () => app,
|
getApp: () => app,
|
||||||
historyStore,
|
historyStore,
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-9
@@ -17,8 +17,9 @@ import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri"
|
|||||||
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
|
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
|
||||||
import { SetupManager } from "@/modules/features/SetupManager";
|
import { SetupManager } from "@/modules/features/SetupManager";
|
||||||
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
|
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
|
||||||
import { useP2PReplicatorCommands } from "@/lib/src/replication/trystero/useP2PReplicatorCommands";
|
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands";
|
||||||
import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature";
|
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
|
||||||
|
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
const SETTINGS_DIR = ".livesync";
|
const SETTINGS_DIR = ".livesync";
|
||||||
const SETTINGS_FILE = "settings.json";
|
const SETTINGS_FILE = "settings.json";
|
||||||
@@ -91,7 +92,7 @@ class LiveSyncWebApp {
|
|||||||
console.log("[Settings] Loaded from .livesync/settings.json");
|
console.log("[Settings] Loaded from .livesync/settings.json");
|
||||||
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.log("[Settings] Failed to load, using defaults");
|
console.log("[Settings] Failed to load, using defaults");
|
||||||
}
|
}
|
||||||
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
||||||
@@ -102,8 +103,8 @@ class LiveSyncWebApp {
|
|||||||
console.log("[AppLifecycle] Restart requested");
|
console.log("[AppLifecycle] Restart requested");
|
||||||
await this.shutdown();
|
await this.shutdown();
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
setTimeout(() => {
|
compatGlobal.setTimeout(() => {
|
||||||
window.location.reload();
|
compatGlobal.location.reload();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ class LiveSyncWebApp {
|
|||||||
const file = await fileHandle.getFile();
|
const file = await fileHandle.getFile();
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
return JSON.parse(text);
|
return JSON.parse(text);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// File doesn't exist yet
|
// File doesn't exist yet
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -235,7 +236,7 @@ class LiveSyncWebApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showError(message: string) {
|
private showError(message: string) {
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = _activeDocument.getElementById("status");
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.className = "error";
|
statusEl.className = "error";
|
||||||
statusEl.textContent = `Error: ${message}`;
|
statusEl.textContent = `Error: ${message}`;
|
||||||
@@ -243,7 +244,7 @@ class LiveSyncWebApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showWarning(message: string) {
|
private showWarning(message: string) {
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = _activeDocument.getElementById("status");
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.className = "warning";
|
statusEl.className = "warning";
|
||||||
statusEl.textContent = `Warning: ${message}`;
|
statusEl.textContent = `Warning: ${message}`;
|
||||||
@@ -251,7 +252,7 @@ class LiveSyncWebApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showSuccess(message: string) {
|
private showSuccess(message: string) {
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = _activeDocument.getElementById("status");
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.className = "success";
|
statusEl.className = "success";
|
||||||
statusEl.textContent = message;
|
statusEl.textContent = message;
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import type {
|
|||||||
IStorageEventWatchHandlers,
|
IStorageEventWatchHandlers,
|
||||||
} from "@lib/managers/adapters";
|
} from "@lib/managers/adapters";
|
||||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||||
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
|
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific type guard adapter
|
* FileSystem API-specific type guard adapter
|
||||||
@@ -149,14 +150,14 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
|
|
||||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||||
// Use FileSystemObserver if available (Chrome 124+)
|
// Use FileSystemObserver if available (Chrome 124+)
|
||||||
if (typeof (window as any).FileSystemObserver === "undefined") {
|
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
|
||||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const FileSystemObserver = (window as any).FileSystemObserver;
|
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
|
||||||
|
|
||||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
@@ -181,7 +182,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
if (changedHandle && changedHandle.kind === "file") {
|
if (changedHandle && changedHandle.kind === "file") {
|
||||||
const file = await changedHandle.getFile();
|
const file = await changedHandle.getFile();
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mtime: file.lastModified,
|
mtime: file.lastModified,
|
||||||
@@ -199,7 +200,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
}
|
}
|
||||||
} else if (type === "disappeared") {
|
} else if (type === "disappeared") {
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: 0,
|
size: 0,
|
||||||
mtime: Date.now(),
|
mtime: Date.now(),
|
||||||
@@ -216,7 +217,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
if (changedHandle && changedHandle.kind === "file") {
|
if (changedHandle && changedHandle.kind === "file") {
|
||||||
const file = await changedHandle.getFile();
|
const file = await changedHandle.getFile();
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
path: relativePath as any,
|
path: relativePath,
|
||||||
stat: {
|
stat: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mtime: file.lastModified,
|
mtime: file.lastModified,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.25.76-webapp",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,9 +11,16 @@
|
|||||||
"run:docker": "docker run -p 8002:80 livesync-webapp",
|
"run:docker": "docker run -p 8002:80 livesync-webapp",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"octagonal-wheels": "^0.1.46"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"svelte": "5.56.3",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^8.0.16",
|
||||||
|
"vite-plugin-istanbul": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
|||||||
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
||||||
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
||||||
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
||||||
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
|
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
|
||||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific implementation of FileAccessBase
|
* FileSystem API-specific implementation of FileAccessBase
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileSystem API-specific implementation of ServiceFileAccess
|
* FileSystem API-specific implementation of ServiceFileAccess
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import { LiveSyncWebApp } from "./main";
|
import { LiveSyncWebApp } from "./main";
|
||||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||||
import type { FilePathWithPrefix } from "@lib/common/types";
|
import type { FilePathWithPrefix } from "@lib/common/types";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Internal state – one app instance per page / browser context
|
// Internal state – one app instance per page / browser context
|
||||||
@@ -41,7 +42,7 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
|||||||
(core.services?.fileProcessing?.processing?.value ?? 0) +
|
(core.services?.fileProcessing?.processing?.value ?? 0) +
|
||||||
(core.services?.replication?.storageApplyingCount?.value ?? 0);
|
(core.services?.replication?.storageApplyingCount?.value ?? 0);
|
||||||
if (q === 0) return;
|
if (q === 0) return;
|
||||||
await new Promise<void>((r) => setTimeout(r, 300));
|
await new Promise<void>((r) => compatGlobal.setTimeout(r, 300));
|
||||||
}
|
}
|
||||||
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ export interface LiveSyncTestAPI {
|
|||||||
const livesyncTest: LiveSyncTestAPI = {
|
const livesyncTest: LiveSyncTestAPI = {
|
||||||
async init(vaultName: string, settings: Partial<ObsidianLiveSyncSettings>): Promise<void> {
|
async init(vaultName: string, settings: Partial<ObsidianLiveSyncSettings>): Promise<void> {
|
||||||
// Clean up any stale OPFS data from previous runs.
|
// Clean up any stale OPFS data from previous runs.
|
||||||
const opfsRoot = await navigator.storage.getDirectory();
|
const opfsRoot = await compatGlobal.navigator.storage.getDirectory();
|
||||||
try {
|
try {
|
||||||
await opfsRoot.removeEntry(vaultName, { recursive: true });
|
await opfsRoot.removeEntry(vaultName, { recursive: true });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -200,4 +201,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Expose on window for Playwright page.evaluate() calls.
|
// Expose on window for Playwright page.evaluate() calls.
|
||||||
(window as any).livesyncTest = livesyncTest;
|
(compatGlobal as any).livesyncTest = livesyncTest;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
||||||
import type { LiveSyncTestAPI } from "../test-entry";
|
import type { LiveSyncTestAPI } from "@/apps/webapp/test-entry";
|
||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
/* Path mapping */
|
/* Path mapping */
|
||||||
"baseUrl": ".",
|
// "baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["../../*"],
|
"@/*": ["../../*"],
|
||||||
"@lib/*": ["../../lib/src/*"]
|
"@lib/*": ["../../lib/src/*"]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
const HANDLE_DB_NAME = "livesync-webapp-handles";
|
const HANDLE_DB_NAME = "livesync-webapp-handles";
|
||||||
const HANDLE_STORE_NAME = "handles";
|
const HANDLE_STORE_NAME = "handles";
|
||||||
const LAST_USED_KEY = "meta:lastUsedVaultId";
|
const LAST_USED_KEY = "meta:lastUsedVaultId";
|
||||||
@@ -89,7 +91,7 @@ export class VaultHistoryStore {
|
|||||||
|
|
||||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||||
return this.withStore("readonly", async (store) => {
|
return this.withStore("readonly", async (store) => {
|
||||||
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
|
const keys = (await this.requestAsPromise(store.getAllKeys()));
|
||||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
||||||
const items: VaultHistoryItem[] = [];
|
const items: VaultHistoryItem[] = [];
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
@@ -170,7 +172,7 @@ export class VaultHistoryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||||
const picker = (window as any).showDirectoryPicker;
|
const picker = (compatGlobal as any).showDirectoryPicker;
|
||||||
if (typeof picker !== "function") {
|
if (typeof picker !== "function") {
|
||||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,20 @@ This pseudo client actually receives the data from other devices, and sends if s
|
|||||||
|
|
||||||
## How to use it?
|
## How to use it?
|
||||||
|
|
||||||
We can build the application by running the following command:
|
We can build the application from the repository root by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ deno task build
|
npm run build -w webpeer
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, open the `dist/index.html` in the browser. It can be configured as the same as the Self-hosted LiveSync (Same components are used[^1]).
|
Or from the package directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/apps/webpeer
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, open `dist/index.html` in the browser. It can be configured in the same way as Self-hosted LiveSync (the same components are used[^1]).
|
||||||
|
|
||||||
## Some notes
|
## Some notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webpeer",
|
"name": "webpeer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.25.76-webpeer",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,15 +11,17 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"octagonal-wheels": "^0.1.46"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.19.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"@tsconfig/svelte": "^5.0.8",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
"svelte": "5.41.1",
|
"svelte": "5.56.3",
|
||||||
"svelte-check": "^443.3",
|
"svelte-check": "^4.6.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^8.0.16"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import {
|
|||||||
type PeerStatus,
|
type PeerStatus,
|
||||||
type PluginShim,
|
type PluginShim,
|
||||||
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||||
import { P2PLogCollector, type P2PReplicatorBase, useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
||||||
|
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
|
||||||
|
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase.ts";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||||
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||||
@@ -28,9 +30,10 @@ import { ServiceContext } from "@lib/services/base/ServiceBase";
|
|||||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
||||||
import { Menu } from "@lib/services/implements/browser/Menu";
|
import { Menu } from "@lib/services/implements/browser/Menu";
|
||||||
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
||||||
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
|
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
|
||||||
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
|
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
|
||||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
function addToList(item: string, list: string) {
|
function addToList(item: string, list: string) {
|
||||||
return unique(
|
return unique(
|
||||||
@@ -137,7 +140,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
|
|
||||||
this._initP2PReplicator();
|
this._initP2PReplicator();
|
||||||
|
|
||||||
setTimeout(() => {
|
compatGlobal.setTimeout(() => {
|
||||||
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
|
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
|
||||||
void this.open();
|
void this.open();
|
||||||
}
|
}
|
||||||
@@ -164,12 +167,12 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
getConfig(key: string) {
|
getConfig(key: string) {
|
||||||
const vaultName = this.services.vault.getVaultName();
|
const vaultName = this.services.vault.getVaultName();
|
||||||
const dbKey = `${vaultName}-${key}`;
|
const dbKey = `${vaultName}-${key}`;
|
||||||
return localStorage.getItem(dbKey);
|
return compatGlobal.localStorage.getItem(dbKey);
|
||||||
}
|
}
|
||||||
setConfig(key: string, value: string) {
|
setConfig(key: string, value: string) {
|
||||||
const vaultName = this.services.vault.getVaultName();
|
const vaultName = this.services.vault.getVaultName();
|
||||||
const dbKey = `${vaultName}-${key}`;
|
const dbKey = `${vaultName}-${key}`;
|
||||||
localStorage.setItem(dbKey, value);
|
compatGlobal.localStorage.setItem(dbKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeviceName(): string {
|
getDeviceName(): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menu } from "@/lib/src/services/implements/browser/Menu";
|
import { Menu } from "@lib/services/implements/browser/Menu";
|
||||||
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
|
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
|
||||||
let result = $state<string | boolean>("");
|
let result = $state<string | boolean>("");
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { mount } from "svelte";
|
import { mount } from "svelte";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
import App from "./App.svelte";
|
import App from "./App.svelte";
|
||||||
|
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById("app")!,
|
target: _activeDocument.getElementById("app")!,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { mount } from "svelte";
|
import { mount } from "svelte";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
import App from "./UITest.svelte";
|
import App from "./UITest.svelte";
|
||||||
|
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById("app")!,
|
target: _activeDocument.getElementById("app")!,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceRoot": "../",
|
// "sourceRoot": "../",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
@@ -15,11 +15,13 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["../../*"],
|
"@/*": ["../../*"],
|
||||||
"@lib/*": ["../../lib/src/*"]
|
"@lib/*": ["../../lib/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
||||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
|
||||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
import { Logger } from "octagonal-wheels/common/logger";
|
import { Logger } from "octagonal-wheels/common/logger";
|
||||||
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LOG_LEVEL_VERBOSE, Logger } from "@/lib/src/common/logger";
|
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
|
||||||
import type { KeyValueDatabase } from "@/lib/src/interfaces/KeyValueDatabase";
|
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase";
|
||||||
import { deleteDB, openDB, type IDBPDatabase } from "idb";
|
import { deleteDB, openDB, type IDBPDatabase } from "idb";
|
||||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { eventHub } from "../lib/src/hub/hub";
|
import { eventHub } from "@lib/hub/hub";
|
||||||
// import type ObsidianLiveSyncPlugin from "../main";
|
// import type ObsidianLiveSyncPlugin from "../main";
|
||||||
|
|
||||||
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
|
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
|
||||||
@@ -43,5 +43,5 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from "../lib/src/events/coreEvents.ts";
|
export * from "@lib/events/coreEvents.ts";
|
||||||
export { eventHub };
|
export { eventHub };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TFile } from "../deps";
|
import type { TFile } from "@/deps";
|
||||||
import type { FilePathWithPrefix, LoadedEntry } from "../lib/src/common/types";
|
import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types";
|
||||||
|
|
||||||
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
|
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
|
|||||||
const r = await requestToCouchDBWithCredentials(
|
const r = await requestToCouchDBWithCredentials(
|
||||||
settings.couchDB_URI,
|
settings.couchDB_URI,
|
||||||
credential,
|
credential,
|
||||||
window.origin,
|
compatGlobal.origin,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
import { type PluginManifest, TFile } from "../deps.ts";
|
import { type PluginManifest, TFile } from "@/deps.ts";
|
||||||
import { type DatabaseEntry, type EntryBody, type FilePath } from "../lib/src/common/types.ts";
|
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
|
||||||
export type { CacheData, FileEventItem } from "../lib/src/common/types.ts";
|
export type { CacheData, FileEventItem } from "@lib/common/types.ts";
|
||||||
|
|
||||||
export interface PluginDataEntry extends DatabaseEntry {
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
deviceVaultName: string;
|
deviceVaultName: string;
|
||||||
@@ -51,7 +51,7 @@ export type queueItem = {
|
|||||||
|
|
||||||
export const FileWatchEventQueueMax = 10;
|
export const FileWatchEventQueueMax = 10;
|
||||||
|
|
||||||
export { configURIBase, configURIBaseQR } from "../lib/src/common/types.ts";
|
export { configURIBase, configURIBaseQR } from "@lib/common/types.ts";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CHeader,
|
CHeader,
|
||||||
@@ -61,4 +61,4 @@ export {
|
|||||||
ICHeaderEnd,
|
ICHeaderEnd,
|
||||||
ICHeaderLength,
|
ICHeaderLength,
|
||||||
ICXHeader,
|
ICXHeader,
|
||||||
} from "../lib/src/common/models/fileaccess.const.ts";
|
} from "@lib/common/models/fileaccess.const.ts";
|
||||||
|
|||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "../deps.ts";
|
import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "@/deps.ts";
|
||||||
import {
|
import {
|
||||||
path2id_base,
|
path2id_base,
|
||||||
id2path_base,
|
id2path_base,
|
||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
isValidFilenameInWidows,
|
isValidFilenameInWidows,
|
||||||
isValidFilenameInAndroid,
|
isValidFilenameInAndroid,
|
||||||
stripAllPrefixes,
|
stripAllPrefixes,
|
||||||
} from "../lib/src/string_and_binary/path.ts";
|
} from "@lib/string_and_binary/path.ts";
|
||||||
|
|
||||||
import { Logger } from "../lib/src/common/logger.ts";
|
import { Logger } from "@lib/common/logger.ts";
|
||||||
import {
|
import {
|
||||||
LOG_LEVEL_INFO,
|
LOG_LEVEL_INFO,
|
||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
@@ -22,14 +22,14 @@ import {
|
|||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
type UXFileInfo,
|
type UXFileInfo,
|
||||||
type UXFileInfoStub,
|
type UXFileInfoStub,
|
||||||
} from "../lib/src/common/types.ts";
|
} from "@lib/common/types.ts";
|
||||||
export { ICHeader, ICXHeader } from "./types.ts";
|
export { ICHeader, ICXHeader } from "./types.ts";
|
||||||
import { writeString } from "../lib/src/string_and_binary/convert.ts";
|
import { writeString } from "@lib/string_and_binary/convert.ts";
|
||||||
import { sameChangePairs } from "./stores.ts";
|
import { sameChangePairs } from "./stores.ts";
|
||||||
|
|
||||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||||
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
|
import { AuthorizationHeaderGenerator } from "@lib/replication/httplib.ts";
|
||||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
|
||||||
|
|
||||||
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
|
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ export const _requestToCouchDBFetch = async (
|
|||||||
method?: string
|
method?: string
|
||||||
) => {
|
) => {
|
||||||
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||||
const encoded = window.btoa(utf8str);
|
const encoded = compatGlobal.btoa(utf8str);
|
||||||
const authHeader = "Basic " + encoded;
|
const authHeader = "Basic " + encoded;
|
||||||
const transformedHeaders: Record<string, string> = {
|
const transformedHeaders: Record<string, string> = {
|
||||||
authorization: authHeader,
|
authorization: authHeader,
|
||||||
@@ -214,7 +214,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.cons
|
|||||||
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
|
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
|
||||||
// Why 2000? : ZIP FILE Does not have enough resolution.
|
// Why 2000? : ZIP FILE Does not have enough resolution.
|
||||||
import { compareMTime } from "@lib/common/utils.ts";
|
import { compareMTime } from "@lib/common/utils.ts";
|
||||||
import { _fetch } from "@/lib/src/common/coreEnvFunctions.ts";
|
import { _fetch, compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
export { compareMTime };
|
export { compareMTime };
|
||||||
function getKey(file: AnyEntry | string | UXFileInfoStub) {
|
function getKey(file: AnyEntry | string | UXFileInfoStub) {
|
||||||
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
|
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
diff_match_patch,
|
diff_match_patch,
|
||||||
Platform,
|
Platform,
|
||||||
addIcon,
|
addIcon,
|
||||||
} from "../../deps.ts";
|
} from "@/deps.ts";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
EntryDoc,
|
EntryDoc,
|
||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
AnyEntry,
|
AnyEntry,
|
||||||
SavingEntry,
|
SavingEntry,
|
||||||
diff_result,
|
diff_result,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "@lib/common/types.ts";
|
||||||
import {
|
import {
|
||||||
CANCELLED,
|
CANCELLED,
|
||||||
LEAVE_TO_SUBSEQUENT,
|
LEAVE_TO_SUBSEQUENT,
|
||||||
@@ -29,8 +29,8 @@ import {
|
|||||||
LOG_LEVEL_VERBOSE,
|
LOG_LEVEL_VERBOSE,
|
||||||
MODE_SELECTIVE,
|
MODE_SELECTIVE,
|
||||||
MODE_SHINY,
|
MODE_SHINY,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "@lib/common/types.ts";
|
||||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "../../common/types.ts";
|
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "@/common/types.ts";
|
||||||
import {
|
import {
|
||||||
createBlob,
|
createBlob,
|
||||||
createSavingEntryFromLoadedEntry,
|
createSavingEntryFromLoadedEntry,
|
||||||
@@ -42,12 +42,12 @@ import {
|
|||||||
isDocContentSame,
|
isDocContentSame,
|
||||||
isLoadedEntry,
|
isLoadedEntry,
|
||||||
isObjectDifferent,
|
isObjectDifferent,
|
||||||
} from "../../lib/src/common/utils.ts";
|
} from "@lib/common/utils.ts";
|
||||||
import { digestHash } from "../../lib/src/string_and_binary/hash.ts";
|
import { digestHash } from "@lib/string_and_binary/hash.ts";
|
||||||
import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
import { arrayBufferToBase64, decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
|
||||||
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
|
||||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
import { stripAllPrefixes } from "@lib/string_and_binary/path.ts";
|
||||||
import {
|
import {
|
||||||
EVEN,
|
EVEN,
|
||||||
disposeMemoObject,
|
disposeMemoObject,
|
||||||
@@ -57,20 +57,20 @@ import {
|
|||||||
memoObject,
|
memoObject,
|
||||||
retrieveMemoObject,
|
retrieveMemoObject,
|
||||||
scheduleTask,
|
scheduleTask,
|
||||||
} from "../../common/utils.ts";
|
} from "@/common/utils.ts";
|
||||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
|
||||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
|
import { pluginScanningCount } from "@lib/mock_and_interop/stores.ts";
|
||||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||||
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
||||||
import { ConflictResolveModal } from "../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
import { ConflictResolveModal } from "@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
|
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "@/common/events.ts";
|
||||||
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
||||||
import { $msg } from "@/lib/src/common/i18n.ts";
|
import { $msg } from "@lib/common/i18n.ts";
|
||||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||||
import type { LiveSyncCore } from "../../main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import { LiveSyncError } from "@lib/common/LSError.ts";
|
import { LiveSyncError } from "@lib/common/LSError.ts";
|
||||||
|
|
||||||
const d = "\u200b";
|
const d = "\u200b";
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
type IPluginDataExDisplay,
|
type IPluginDataExDisplay,
|
||||||
type PluginDataExFile,
|
type PluginDataExFile,
|
||||||
} from "./CmdConfigSync.ts";
|
} from "./CmdConfigSync.ts";
|
||||||
import { Logger } from "../../lib/src/common/logger";
|
import { Logger } from "@lib/common/logger";
|
||||||
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
|
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types";
|
||||||
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
|
import { getDocData, timeDeltaToHumanReadable, unique } from "@lib/common/utils";
|
||||||
import type ObsidianLiveSyncPlugin from "../../main";
|
import type ObsidianLiveSyncPlugin from "@/main";
|
||||||
// import { askString } from "../../common/utils";
|
// import { askString } from "../../common/utils";
|
||||||
import { Menu } from "@/deps.ts";
|
import { Menu } from "@/deps.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mount, unmount } from "svelte";
|
import { mount, unmount } from "svelte";
|
||||||
import { App, Modal } from "../../deps.ts";
|
import { App, Modal } from "@/deps.ts";
|
||||||
import ObsidianLiveSyncPlugin from "../../main.ts";
|
import ObsidianLiveSyncPlugin from "@/main.ts";
|
||||||
import PluginPane from "./PluginPane.svelte";
|
import PluginPane from "./PluginPane.svelte";
|
||||||
export class PluginDialogModal extends Modal {
|
export class PluginDialogModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
@@ -16,9 +16,11 @@ export class PluginDialogModal extends Modal {
|
|||||||
|
|
||||||
override onOpen() {
|
override onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
this.contentEl.style.overflow = "auto";
|
this.contentEl.setCssStyles({
|
||||||
this.contentEl.style.display = "flex";
|
overflow: "auto",
|
||||||
this.contentEl.style.flexDirection = "column";
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
});
|
||||||
this.titleEl.setText("Customization Sync (Beta3)");
|
this.titleEl.setText("Customization Sync (Beta3)");
|
||||||
if (!this.component) {
|
if (!this.component) {
|
||||||
this.component = mount(PluginPane, {
|
this.component = mount(PluginPane, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import ObsidianLiveSyncPlugin from "../../main";
|
import ObsidianLiveSyncPlugin from "@/main";
|
||||||
import {
|
import {
|
||||||
ConfigSync,
|
ConfigSync,
|
||||||
type IPluginDataExDisplay,
|
type IPluginDataExDisplay,
|
||||||
@@ -11,16 +11,16 @@
|
|||||||
} from "./CmdConfigSync.ts";
|
} from "./CmdConfigSync.ts";
|
||||||
import PluginCombo from "./PluginCombo.svelte";
|
import PluginCombo from "./PluginCombo.svelte";
|
||||||
import { Menu, type PluginManifest } from "@/deps.ts";
|
import { Menu, type PluginManifest } from "@/deps.ts";
|
||||||
import { unique } from "../../lib/src/common/utils";
|
import { unique } from "@lib/common/utils";
|
||||||
import {
|
import {
|
||||||
MODE_SELECTIVE,
|
MODE_SELECTIVE,
|
||||||
MODE_AUTOMATIC,
|
MODE_AUTOMATIC,
|
||||||
MODE_PAUSED,
|
MODE_PAUSED,
|
||||||
type SYNC_MODE,
|
type SYNC_MODE,
|
||||||
MODE_SHINY,
|
MODE_SHINY,
|
||||||
} from "../../lib/src/common/types";
|
} from "@lib/common/types";
|
||||||
import { normalizePath } from "../../deps";
|
import { normalizePath } from "@/deps";
|
||||||
import { HiddenFileSync } from "../HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||||
export let plugin: ObsidianLiveSyncPlugin;
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { App, Modal } from "../../deps.ts";
|
import { App, Modal } from "@/deps.ts";
|
||||||
import { type FilePath, type LoadedEntry } from "../../lib/src/common/types.ts";
|
import { type FilePath, type LoadedEntry } from "@lib/common/types.ts";
|
||||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||||
import { waitForSignal } from "../../lib/src/common/utils.ts";
|
import { waitForSignal } from "@lib/common/utils.ts";
|
||||||
import { mount, unmount } from "svelte";
|
import { mount, unmount } from "svelte";
|
||||||
|
|
||||||
export class JsonResolveModal extends Modal {
|
export class JsonResolveModal extends Modal {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "../../deps.ts";
|
import { type Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "@/deps.ts";
|
||||||
import type { FilePath, LoadedEntry } from "../../lib/src/common/types.ts";
|
import type { FilePath, LoadedEntry } from "@lib/common/types.ts";
|
||||||
import { decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
import { decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
|
||||||
import { getDocData, isObjectDifferent, mergeObject } from "../../lib/src/common/utils.ts";
|
import { getDocData, isObjectDifferent, mergeObject } from "@lib/common/utils.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
docs?: LoadedEntry[];
|
docs?: LoadedEntry[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PluginManifest, type ListedFiles } from "../../deps.ts";
|
import { type PluginManifest, type ListedFiles } from "@/deps.ts";
|
||||||
import {
|
import {
|
||||||
type LoadedEntry,
|
type LoadedEntry,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
LOG_LEVEL_DEBUG,
|
LOG_LEVEL_DEBUG,
|
||||||
type MetaEntry,
|
type MetaEntry,
|
||||||
type UXDataWriteOptions,
|
type UXDataWriteOptions,
|
||||||
} from "../../lib/src/common/types.ts";
|
} from "@lib/common/types.ts";
|
||||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../../common/types.ts";
|
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "@/common/types.ts";
|
||||||
import {
|
import {
|
||||||
readAsBlob,
|
readAsBlob,
|
||||||
isDocContentSame,
|
isDocContentSame,
|
||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
fireAndForget,
|
fireAndForget,
|
||||||
type CustomRegExp,
|
type CustomRegExp,
|
||||||
getFileRegExp,
|
getFileRegExp,
|
||||||
} from "../../lib/src/common/utils.ts";
|
} from "@lib/common/utils.ts";
|
||||||
import {
|
import {
|
||||||
compareMTime,
|
compareMTime,
|
||||||
isInternalMetadata,
|
isInternalMetadata,
|
||||||
@@ -39,17 +39,17 @@ import {
|
|||||||
BASE_IS_NEW,
|
BASE_IS_NEW,
|
||||||
EVEN,
|
EVEN,
|
||||||
displayRev,
|
displayRev,
|
||||||
} from "../../common/utils.ts";
|
} from "@/common/utils.ts";
|
||||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||||
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
|
||||||
import { addPrefix, stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
import { addPrefix, stripAllPrefixes } from "@lib/string_and_binary/path.ts";
|
||||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||||
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src/mock_and_interop/stores.ts";
|
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "@lib/mock_and_interop/stores.ts";
|
||||||
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
|
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
|
||||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||||
import type { LiveSyncCore } from "../../main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import { tryGetFilePath } from "@lib/common/utils.doc.ts";
|
import { tryGetFilePath } from "@lib/common/utils.doc.ts";
|
||||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
type FilePath,
|
type FilePath,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
type LOG_LEVEL,
|
type LOG_LEVEL,
|
||||||
} from "../lib/src/common/types.ts";
|
} from "@lib/common/types.ts";
|
||||||
import type ObsidianLiveSyncPlugin from "../main.ts";
|
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||||
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
|
import { MARK_DONE } from "@/modules/features/ModuleLog.ts";
|
||||||
import type { LiveSyncCore } from "../main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import { __$checkInstanceBinding } from "../lib/src/dev/checks.ts";
|
import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
|
||||||
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
|
import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts";
|
||||||
|
|
||||||
let noticeIndex = 0;
|
let noticeIndex = 0;
|
||||||
export abstract class LiveSyncCommands {
|
export abstract class LiveSyncCommands {
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import {
|
|||||||
type EntryLeaf,
|
type EntryLeaf,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
type MetaEntry,
|
type MetaEntry,
|
||||||
} from "../../lib/src/common/types";
|
} from "@lib/common/types";
|
||||||
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
import { getNoFromRev } from "@lib/pouchdb/LiveSyncLocalDB";
|
||||||
import { LiveSyncCommands } from "../LiveSyncCommands";
|
import { LiveSyncCommands } from "@/features/LiveSyncCommands";
|
||||||
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||||
import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||||
import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events";
|
import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events";
|
||||||
import type { LiveSyncCouchDBReplicator } from "@/lib/src/replication/couchdb/LiveSyncReplicator";
|
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||||
import { delay } from "@/lib/src/common/utils";
|
import { delay } from "@lib/common/utils";
|
||||||
// import { _requestToCouchDB } from "@/common/utils";
|
// import { _requestToCouchDB } from "@/common/utils";
|
||||||
const DB_KEY_SEQ = "gc-seq";
|
const DB_KEY_SEQ = "gc-seq";
|
||||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { App, Modal } from "@/deps.ts";
|
import { App, Modal } from "@/deps.ts";
|
||||||
import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte";
|
import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte";
|
||||||
import { mount, unmount } from "svelte";
|
import { mount, unmount } from "svelte";
|
||||||
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
|
|
||||||
export type P2POpenReplicationModalCallback = {
|
export type P2POpenReplicationModalCallback = {
|
||||||
onSync: (peerId: string) => Promise<void>;
|
onSync: (peerId: string) => Promise<void>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
// import type { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
|
// import type { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
|
||||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types";
|
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types";
|
||||||
import { Logger } from "@lib/common/logger";
|
import { Logger } from "@lib/common/logger";
|
||||||
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||||
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
|
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user