Compare commits

...

39 Commits

Author SHA1 Message Date
vorotamoroz
72033472f3 add dependency explicitly 2026-06-17 10:36:38 +01:00
vorotamoroz
93dc03e86f remove unused dependencies, update some dependencies 2026-06-17 10:05:45 +01:00
vorotamoroz
dae8443fe8 update more deps 2026-06-17 06:35:14 +01:00
vorotamoroz
88a8bcbd5a barrel node specific modules to summarise the warnings 2026-06-17 06:15:25 +01:00
vorotamoroz
4a5283543d for automatic review 2026-06-17 06:10:30 +01:00
vorotamoroz
7895336189 revert deno test 2026-06-17 06:08:40 +01:00
vorotamoroz
2d5cdccf7d for automatic review 2026-06-17 05:51:01 +01:00
vorotamoroz
497fd04081 fix global references 2026-06-17 05:29:45 +01:00
vorotamoroz
ae9c46f8f0 Fix import paths 2026-06-17 04:39:39 +01:00
vorotamoroz
dcd10cd690 Fix dependency management 2026-06-17 04:24:55 +01:00
vorotamoroz
5a35b71339 define workspace 2026-06-17 04:01:45 +01:00
vorotamoroz
18a59219f7 add ignore paths 2026-06-15 12:25:50 +01:00
vorotamoroz
c9095738e6 (chore): limit tags 2026-06-15 12:21:58 +01:00
vorotamoroz
bed415fc6b (chore): add missing release date 2026-06-15 12:21:11 +01:00
vorotamoroz
4eeb93b9e4 Merge pull request #961 from vrtmrz/0_25_76
Releasing 0.25.76
2026-06-15 20:18:37 +09:00
vorotamoroz
0fc233686b prettify and bump 2026-06-15 11:54:41 +01:00
vorotamoroz
def1d297f5 Merge pull request #958 from vrtmrz/fix_875
Fix S3 client with custom headers and strict server
2026-06-15 19:41:31 +09:00
vorotamoroz
e7cf2a6fba Merge branch 'main' into fix_875 2026-06-15 19:41:18 +09:00
vorotamoroz
2da2fd7671 Merge pull request #960 from vrtmrz/fix_956
Fix configuration serialisation
2026-06-15 19:40:37 +09:00
vorotamoroz
d5175969e7 Update notes. 2026-06-15 11:39:38 +01:00
vorotamoroz
46f9630999 track merged submodule 2026-06-15 11:39:13 +01:00
vorotamoroz
75adcf0ff0 track merged submodule 2026-06-15 11:38:50 +01:00
vorotamoroz
866dd49ba3 ### Fixed
- No longer connection information of the P2P synchronisation is broken on the specific platform (#956).
2026-06-15 11:34:25 +01:00
vorotamoroz
d0a84d07aa (chore) typo 2026-06-15 10:43:27 +01:00
vorotamoroz
089a4c0f8b ### Fixed
- Now S3 connection with custom headers works properly.
2026-06-15 10:41:26 +01:00
vorotamoroz
1a1f816872 Merge pull request #957 from vrtmrz/v0_25_75
Releasing v0.25.75
2026-06-13 12:05:16 +09:00
vorotamoroz
9d86c2828b Update dependency, and bump 2026-06-13 11:58:40 +09:00
vorotamoroz
3b6d3beaa7 Merge pull request #955 from vrtmrz/fix_953
Fix an issue where using fast synchronisation caused a TypeError in some environment
2026-06-13 11:21:19 +09:00
vorotamoroz
bb75b6ead8 Merge remote-tracking branch 'origin/main' into fix_953 2026-06-13 11:19:22 +09:00
vorotamoroz
fccb2304f6 update submodule pointer 2026-06-13 11:18:03 +09:00
vorotamoroz
f00ef5eaae update submodule pointer 2026-06-13 11:15:03 +09:00
vorotamoroz
4e7ee760de Merge pull request #949 from AutoraLabs/feat/keep-livesync-active-in-background
feat: opt-in desktop setting to keep replication active in the background
2026-06-13 11:14:04 +09:00
vorotamoroz
c4faade30c Update submodule pointer 2026-06-13 11:13:35 +09:00
vorotamoroz
295dc1392a ### Fixed
- Fixed an issue where using fast synchronisation caused a TypeError in some environments (#953).
2026-06-12 11:33:07 +01:00
Miguel Ferreira
445a8c747c chore(submodule): bump src/lib to commonlib main (#51 merged as e98d929)
vrtmrz/livesync-commonlib#51 is merged into commonlib main as e98d929. Repoint
the gitlink from the PR branch commit to that merge commit so this PR builds
against upstream main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:45:40 +01:00
Miguel Ferreira
292a6b9e1e refactor: detect platform via APIService.isMobile() instead of Platform.isDesktopApp
Address the maintainer review on #949: determine the platform through the
plugin's own service layer (services.API.isMobile()) rather than Obsidian's
Platform API directly, matching the existing call in ObsidianLiveSyncSettingTab.
Applies to both PR-introduced sites: the runtime guard (ModuleObsidianEvents)
and the settings-pane toggle (PaneSyncSettings).

The TFile import becomes type-only so deps.ts is no longer pulled at runtime;
the unit test drives the platform through the services.API.isMobile() mock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:45:40 +01:00
vorotamoroz
0e04e7d31d Merge pull request #951 from vrtmrz/feat_docker_ci_build
feat: Docker CI workflow to enhance image tagging and push logic base…
2026-06-09 18:41:26 +09:00
vorotamoroz
4cf4acf7e9 feat: Docker CI workflow to enhance image tagging and push logic based on branch and event type 2026-06-09 09:00:43 +00:00
Miguel Ferreira
c78e583399 feat: opt-in desktop setting to keep replication active in the background
Replication is suspended when the Obsidian window becomes hidden (document.hidden),
so LiveSync and Periodic stop syncing while minimised until the window is focused.

Add keepReplicationActiveInBackground (default off, desktop only). When enabled, the
window-visibility handler no longer suspends on hide, so replication keeps running while
minimised. Becoming visible forces a teardown before reopen (LiveSync only) so a stalled,
half-open channel is always replaced.

Includes the setting definition (src/lib submodule), a desktop-only toggle in the Sync
pane shown for LiveSync and Periodic, a docs/settings.md entry, and unit tests for the
visibility handler.
2026-06-05 00:06:26 +01:00
198 changed files with 5130 additions and 3183 deletions

View File

@@ -8,8 +8,21 @@ name: Build and Push CLI Docker Image
on:
push:
branches:
- main
tags:
- "*.*.*-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:
inputs:
dry_run:
@@ -41,14 +54,32 @@ jobs:
id: meta
run: |
VERSION=$(jq -r '.version' manifest.json)
EPOCH=$(date +%s)
TAG="${VERSION}-${EPOCH}-cli"
SHORT_SHA=$(git rev-parse --short HEAD)
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
echo "full=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT
echo "version=${IMAGE}:${VERSION}-cli" >> $GITHUB_OUTPUT
echo "latest=${IMAGE}:latest" >> $GITHUB_OUTPUT
# Build tag list based on the event and git ref
TAGS=""
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
# 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
uses: docker/login-action@v3
@@ -92,10 +123,7 @@ jobs:
with:
context: .
file: src/apps/cli/Dockerfile
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
tags: |
${{ steps.meta.outputs.full }}
${{ steps.meta.outputs.version }}
${{ steps.meta.outputs.latest }}
push: ${{ steps.meta.outputs.push }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -4,6 +4,7 @@ on:
# Sequence of patterns matched against refs/tags
tags:
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
- '!*-cli' # Exclude command-line interface tags
workflow_dispatch:
jobs:

View File

@@ -1,68 +1,111 @@
# Run Unit test without Harnesses
name: unit-ci
on:
workflow_dispatch:
push:
branches:
- main
- beta
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'vite.config.ts'
- 'vitest.config*.ts'
- 'esbuild.config.mjs'
- 'eslint.config.mjs'
- '.github/workflows/unit-ci.yml'
pull_request:
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'vite.config.ts'
- 'vitest.config*.ts'
- 'esbuild.config.mjs'
- 'eslint.config.mjs'
- '.github/workflows/unit-ci.yml'
permissions:
contents: read
jobs:
test:
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
# unit tests do not require Playwright, so we can skip installing its dependencies to save time
# - name: Install test dependencies (Playwright Chromium)
# run: npm run test:install-dependencies
- name: Run unit tests suite with coverage
run: npm run test:unit:coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/**
# Run Unit test without Harnesses
name: unit-ci
on:
workflow_dispatch:
push:
branches:
- main
- beta
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'vite.config.ts'
- 'vitest.config*.ts'
- 'esbuild.config.mjs'
- 'eslint.config.mjs'
- '.github/workflows/unit-ci.yml'
pull_request:
paths:
- 'src/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'vite.config.ts'
- 'vitest.config*.ts'
- 'esbuild.config.mjs'
- 'eslint.config.mjs'
- '.github/workflows/unit-ci.yml'
permissions:
contents: read
jobs:
unit-test:
name: Unit 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: Run unit tests suite with coverage
run: npm run test:unit:coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-coverage-report
path: coverage/**
integration-test:
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

16
devs.md
View File

@@ -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`)~~
- This is now obsolete, migrated to vitest.
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests.
- Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`).
- **Vitest**:
- **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:
```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: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.
- **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/harness/` - Mock implementations (e.g., `obsidian-mock.ts`)
@@ -151,7 +156,7 @@ Hence, the new feature should be implemented as follows:
## Common Patterns
### Module Implementation
### Module Implementation (Now not recommended for new features, use services instead)
```typescript
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
- 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).
- 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).

View File

@@ -488,6 +488,11 @@ Automatically Sync all files when opening Obsidian.
Setting key: syncAfterMerge
Sync automatically after merging files
#### Keep replication active in the background
Setting key: keepReplicationActiveInBackground
Desktop only; uses more battery and network.
### 3. Update thinning
#### Batch database update

View File

@@ -19,11 +19,15 @@ const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
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) {
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
} 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 = {
@@ -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[] */
const plugins = [
{
@@ -177,6 +209,7 @@ const context = await esbuild.context({
preprocess: sveltePreprocess(),
compilerOptions: { css: "injected", preserveComments: false },
}),
removePragmaCommentsPlugin,
...plugins,
],
});

143
eslint.config.common.mjs Normal file
View File

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

View File

@@ -4,6 +4,8 @@ import globals from "globals";
import { defineConfig, globalIgnores } from "eslint/config";
import * as sveltePlugin from "eslint-plugin-svelte";
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.
export default defineConfig([
globalIgnores([
@@ -18,11 +20,11 @@ export default defineConfig([
"**/*.json",
"**/.eslintrc.js.bak",
// 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)
"src/apps/**/*",
"utils/**/*",
"src/apps",
"utils",
// Specific exclusions from common library (src/lib)
"src/lib/coverage",
@@ -54,6 +56,7 @@ export default defineConfig([
]),
...sveltePlugin.configs["flat/base"],
...obsidianmd.configs.recommended,
importAlias.configs.recommended,
{
files: ["**/*.ts"],
// 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,
parserOptions: {
project: "./tsconfig.json",
rootDir: "./",
},
},
linterOptions:{
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
// -- 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
// @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",
...baseRules,
...obsidianRules,
// -- Project specific rules
...ImportAliasRules("."),
},
},
{
files: ["**/*.svelte"],
languageOptions: {
globals: { ...globals.browser, PouchDB: "readonly" },
parser: svelteParser,
parserOptions: {
parser: tsParser,
extraFileExtensions: [".svelte"],
project: "./tsconfig.json",
rootDir: "./",
},
},
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.
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
"no-unused-vars": "off",
"obsidianmd/no-plugin-as-component": "off",
"obsidianmd/ui/sentence-case": "off",
...obsidianRules,
...ImportAliasRules("."),
},
},
]);

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.74",
"version": "0.25.76",
"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.",
"author": "vorotamoroz",

3941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"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.",
"main": "main.js",
"type": "module",
@@ -25,10 +25,12 @@
"pretty": "npm run prettyNoWrite -- --write --log-level error",
"prettyCheck": "npm run prettyNoWrite -- --check",
"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/",
"test": "vitest run",
"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:install-playwright": "npx playwright install chromium",
"test:install-dependencies": "npm run test:install-playwright",
@@ -55,15 +57,17 @@
"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: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": [],
"author": "vorotamoroz",
"license": "MIT",
"devDependencies": {
"@chialab/esbuild-plugin-worker": "^0.19.0",
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@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",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
@@ -78,23 +82,21 @@
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild": "0.28.1",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-svelte": "^3.15.0",
"eslint-plugin-svelte": "^3.19.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"postcss-load-config": "^6.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
@@ -108,20 +110,22 @@
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.4.3",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tslib": "^2.8.1",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vite-plugin-istanbul": "^8.0.0",
"typescript-eslint": "^8.61.0",
"vite": "^8.0.16",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
"yaml": "^2.8.2",
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
@@ -130,21 +134,22 @@
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"commander": "^14.0.3",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"micromatch": "^4.0.0",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"markdown-it": "^14.2.0",
"minimatch": "^10.2.5",
"obsidian": "^1.13.1",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}
},
"workspaces": [
"src/apps/cli",
"src/apps/webpeer",
"src/apps/webapp"
]
}

View File

@@ -82,8 +82,8 @@ RUN apt-get update \
WORKDIR /deps
# runtime-package.json lists only the packages that Vite leaves external
COPY src/apps/cli/runtime-package.json ./package.json
# package.json lists only the packages that the CLI requires
COPY src/apps/cli/package.json ./package.json
RUN npm install --omit=dev
# ─────────────────────────────────────────────────────────────────────────────

View File

@@ -118,19 +118,26 @@ git submodule update --init --recursive
# Install dependencies from the repository root
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
npm run build
```
If `src/lib` is missing, `npm run build` now stops early with a targeted message
instead of a low-level Vite `ENOENT` error.
If `src/lib` is missing, the build process stops early with a targeted message instead of a low-level Vite `ENOENT` error.
Run the CLI:
```bash
# Run with npm script (from repository root)
npm run --silent cli -- [database-path] [command] [args...]
# Run with npm workspace script (from repository root)
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
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
```

View File

@@ -1,7 +1,7 @@
import * as path from "path";
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { path } from "../node-compat";
/**
* Conversion adapter implementation for Node.js

View File

@@ -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 { IFileSystemAdapter } from "@lib/serviceModules/adapters";
import { NodePathAdapter } from "./NodePathAdapter";
@@ -8,6 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
import { NodeStorageAdapter } from "./NodeStorageAdapter";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "../node-compat";
/**
* Complete file system adapter implementation for Node.js

View File

@@ -1,7 +1,7 @@
import * as path from "path";
import type { FilePath } from "@lib/common/types";
import type { IPathAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile } from "./NodeTypes";
import { path } from "../node-compat";
/**
* Path adapter implementation for Node.js

View File

@@ -1,8 +1,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { UXDataWriteOptions } from "@lib/common/types";
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
import type { NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "../node-compat";
/**
* Storage adapter implementation for Node.js

View File

@@ -1,8 +1,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { UXDataWriteOptions } from "@lib/common/types";
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

View File

@@ -1,15 +1,16 @@
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { addP2PEventHandlers } from "@lib/replication/trystero/addP2PEventHandlers";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
type CLIP2PPeer = {
peerId: string;
name: string;
};
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 {

View File

@@ -1,5 +1,3 @@
import * as fs from "fs/promises";
import * as path from "path";
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
import { configURIBase } from "@lib/common/models/shared.const";
import {
@@ -18,6 +16,8 @@ import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelative
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
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 {
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 () => {
clearTimeout(pollTimer);
compatGlobal.clearTimeout(pollTimer);
return true;
});
} else {

View File

@@ -1,4 +1,4 @@
import { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";

View File

@@ -1,5 +1,4 @@
import * as path from "path";
import * as readline from "node:readline/promises";
import { path, readline } from "../node-compat";
export function toArrayBuffer(data: Buffer): ArrayBuffer {
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// eslint-disable -- This is the entry point for the CLI application.
import * as polyfill from "werift";
import { main } from "./main";

View File

@@ -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 { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
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 { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import {
LOG_LEVEL_DEBUG,
setGlobalLogFunction,
@@ -26,7 +19,8 @@ import type { CLICommand, CLIOptions } from "./commands/types";
import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
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";
ensureGlobalNodeLocalStorage();
@@ -238,8 +232,8 @@ async function createDefaultSettingsFile(options: CLIOptions) {
const targetPath = options.settingsPath
? path.resolve(options.settingsPath)
: options.commandArgs[0]
? path.resolve(options.commandArgs[0])
: path.resolve(process.cwd(), "data.json");
? path.resolve(options.commandArgs[0])
: path.resolve(process.cwd(), "data.json");
if (!options.force) {
try {
@@ -329,8 +323,8 @@ export async function main() {
options.command === "mirror" && options.commandArgs[0]
? path.resolve(options.commandArgs[0])
: options.vaultPath
? path.resolve(options.vaultPath)
: databasePath!;
? path.resolve(options.vaultPath)
: databasePath!;
// Check if vault directory exists
try {
@@ -485,8 +479,8 @@ export async function main() {
}
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
// Save the settings file before any lifecycle events can mutate and persist them.
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
@@ -499,8 +493,8 @@ export async function main() {
if (settingsBackup) {
const tmpPath = settingsPath + ".tmp";
try {
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
require("fs").renameSync(tmpPath, settingsPath);
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
fsSync.renameSync(tmpPath, settingsPath);
} catch (err) {
console.error("[Settings] Failed to restore settings on exit:", err);
}
@@ -563,7 +557,7 @@ export async function main() {
if (options.command === "daemon" && result) {
// Keep the process running
await new Promise(() => {});
await new Promise(() => { });
} else {
await core.services.control.onUnload();
}

View File

@@ -10,12 +10,10 @@ import type {
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
import type { NodeFile, NodeFolder } from "../adapters/NodeTypes";
import type { Stats } from "fs";
import * as fs from "fs/promises";
import * as path from "path";
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
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
@@ -101,7 +99,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
private basePath: string,
private ignoreRules?: IgnoreRules,
private watchEnabled: boolean = false
) {}
) { }
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
return {

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
import type { NodeFile } from "../adapters/NodeTypes";
import type { NodeFile } from "@/apps/cli/adapters/NodeTypes";
// ── chokidar mock ──────────────────────────────────────────────────────────────
// Must be hoisted before imports that pull in chokidar.

View File

@@ -1,8 +1,8 @@
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
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 { IgnoreRules } from "../serviceModules/IgnoreRules";
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "self-hosted-livesync-cli",
"private": true,
"version": "0.0.0",
"version": "0.25.76-cli",
"main": "dist/index.cjs",
"type": "module",
"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: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": {},
"devDependencies": {}
"dependencies": {
"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"
}
}

View File

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

View File

@@ -1,13 +1,13 @@
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
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 { FileAccessCLI } from "./FileAccessCLI";
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
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 { IgnoreRules } from "./IgnoreRules";

View File

@@ -1,5 +1,5 @@
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

View File

@@ -1,7 +1,5 @@
import * as fs from "fs/promises";
import * as path from "path";
import { minimatch } from "minimatch";
import { fsPromises as fs, path } from "../node-compat";
/**
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.

View File

@@ -1,5 +1,5 @@
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

View File

@@ -7,8 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
import type { IVaultService } from "@lib/services/base/IService";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
import * as nodeFs from "node:fs";
import * as nodePath from "node:path";
import { fs as nodeFs, path as nodePath } from "../node-compat";
const NODE_KV_TYPED_KEY = "__nodeKvType";
const NODE_KV_VALUES_KEY = "values";

View File

@@ -1,5 +1,4 @@
import * as nodeFs from "node:fs";
import * as nodePath from "node:path";
import { fs as nodeFs, path as nodePath } from "../node-compat";
type LocalStorageShape = {
getItem(key: string): string | null;

View File

@@ -1,6 +1,5 @@
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import * as nodePath from "node:path";
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
import { UIService } from "@lib/services/implements/base/UIService";
@@ -24,7 +23,8 @@ import type { ServiceInstances } from "@lib/services/ServiceHub";
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
import { NodeSettingService } from "./NodeSettingService";
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 {
databasePath: string;

View File

@@ -77,7 +77,9 @@ export class BackgroundCliProcess {
if (this.combined.includes(needle)) return;
const status = await Promise.race([
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") {
throw new Error(

View File

@@ -109,7 +109,11 @@ Deno.test("daemon: ignore rules behaviour", async (t) => {
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");
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");
});

View File

@@ -46,9 +46,7 @@ Deno.test("decoupled database and vault", async () => {
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."
);
console.warn("[WARN] CouchDB environment variables are not fully set. Push and pull operations may fail.");
await markSettingsConfigured(settingsFile);
}
@@ -59,29 +57,11 @@ Deno.test("decoupled database and vault", async () => {
// 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
);
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
);
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");
@@ -93,14 +73,7 @@ Deno.test("decoupled database and vault", async () => {
// 4. Test mirror command with decoupled vault directory
console.log("[INFO] mirror with decoupled vault");
await runCliOrFail(
dbDir,
"--vault",
vaultDir,
"--settings",
settingsFile,
"mirror"
);
await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", settingsFile, "mirror");
const restoredFile = join(vaultDir, REMOTE_PATH);
const restored = await Deno.readTextFile(restoredFile);

View File

@@ -14,7 +14,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p-peers: discovers host through local relay", async () => {
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 passphrase = Deno.env.get("PASSPHRASE") ?? "test";

View File

@@ -15,7 +15,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p-sync: discovers peer and completes sync", async () => {
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 passphrase = Deno.env.get("PASSPHRASE") ?? "test";

View File

@@ -15,7 +15,7 @@ import { getOptimalLoopbackIp } from "./helpers/net.ts";
Deno.test("p2p: three nodes detect and resolve conflicts", async () => {
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 passphrase = Deno.env.get("PASSPHRASE") ?? "test";

View File

@@ -61,11 +61,7 @@ Deno.test("remote management commands", async () => {
// 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"
);
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

View File

@@ -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.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.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");
console.log("[PASS] info output format matched");
});

View File

@@ -15,18 +15,18 @@
"noEmit": true,
/* Linting */
"strict": false,
"strict": true,
// "noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
// "rootDir": "../../../",
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "test", "testdeno"]
}

View File

@@ -35,8 +35,15 @@ npm install
### Development
From the repository root:
```bash
npm run dev -w livesync-webapp
```
Or from the package directory:
```bash
# Build the project (ensure you are in `src/apps/webapp` directory)
cd src/apps/webapp
npm run dev
```
@@ -45,8 +52,15 @@ This will start a development server at `http://localhost:3000`.
### Build
From the repository root:
```bash
npm run build -w livesync-webapp
```
Or from the package directory:
```bash
# Build the project (ensure you are in `src/apps/webapp` directory)
cd src/apps/webapp
npm run build
```

View File

@@ -1,11 +1,12 @@
import { LiveSyncWebApp } from "./main";
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const historyStore = new VaultHistoryStore();
let app: LiveSyncWebApp | null = null;
function getRequiredElement<T extends HTMLElement>(id: string): T {
const element = document.getElementById(id);
const element = _activeDocument.getElementById(id);
if (!element) {
throw new Error(`Missing element: #${id}`);
}
@@ -22,7 +23,7 @@ function setBusyState(isBusy: boolean): void {
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
pickNewBtn.disabled = isBusy;
const historyButtons = document.querySelectorAll<HTMLButtonElement>(".vault-item button");
const historyButtons = _activeDocument.querySelectorAll<HTMLButtonElement>(".vault-item button");
historyButtons.forEach((button) => {
button.disabled = isBusy;
});
@@ -45,24 +46,24 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
emptyEl.classList.toggle("is-hidden", items.length > 0);
for (const item of items) {
const row = document.createElement("div");
const row = _activeDocument.createElement("div");
row.className = "vault-item";
const info = document.createElement("div");
const info = _activeDocument.createElement("div");
info.className = "vault-item-info";
const name = document.createElement("div");
const name = _activeDocument.createElement("div");
name.className = "vault-item-name";
name.textContent = item.name;
const meta = document.createElement("div");
const meta = _activeDocument.createElement("div");
meta.className = "vault-item-meta";
const label = item.id === lastUsedId ? "Last used" : "Used";
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
info.append(name, meta);
const useButton = document.createElement("button");
const useButton = _activeDocument.createElement("button");
useButton.type = "button";
useButton.textContent = "Use this vault";
useButton.addEventListener("click", () => {
@@ -120,7 +121,7 @@ async function initializeVaultSelector(): Promise<void> {
await renderHistoryList();
}
window.addEventListener("load", async () => {
compatGlobal.addEventListener("load", async () => {
try {
await initializeVaultSelector();
} catch (error) {
@@ -129,11 +130,11 @@ window.addEventListener("load", async () => {
}
});
window.addEventListener("beforeunload", () => {
compatGlobal.addEventListener("beforeunload", () => {
void app?.shutdown();
});
(window as any).livesyncApp = {
(compatGlobal as any).livesyncApp = {
getApp: () => app,
historyStore,
};

View File

@@ -17,8 +17,9 @@ import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri"
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
import { SetupManager } from "@/modules/features/SetupManager";
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
import { useP2PReplicatorCommands } from "@/lib/src/replication/trystero/useP2PReplicatorCommands";
import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature";
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands";
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json";
@@ -91,7 +92,7 @@ class LiveSyncWebApp {
console.log("[Settings] Loaded from .livesync/settings.json");
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
}
} catch (error) {
} catch {
console.log("[Settings] Failed to load, using defaults");
}
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
@@ -102,8 +103,8 @@ class LiveSyncWebApp {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
setTimeout(() => {
window.location.reload();
compatGlobal.setTimeout(() => {
compatGlobal.location.reload();
}, 1000);
});
@@ -169,7 +170,7 @@ class LiveSyncWebApp {
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
} catch (error) {
} catch {
// File doesn't exist yet
return null;
}
@@ -235,7 +236,7 @@ class LiveSyncWebApp {
}
private showError(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "error";
statusEl.textContent = `Error: ${message}`;
@@ -243,7 +244,7 @@ class LiveSyncWebApp {
}
private showWarning(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "warning";
statusEl.textContent = `Warning: ${message}`;
@@ -251,7 +252,7 @@ class LiveSyncWebApp {
}
private showSuccess(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "success";
statusEl.textContent = message;

View File

@@ -10,7 +10,8 @@ import type {
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
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
@@ -149,14 +150,14 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// 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] Consider using Chrome 124+ for real-time file watching");
return Promise.resolve();
}
try {
const FileSystemObserver = (window as any).FileSystemObserver;
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
this.observer = new FileSystemObserver(async (records: any[]) => {
for (const record of records) {
@@ -181,7 +182,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,
@@ -199,7 +200,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
}
} else if (type === "disappeared") {
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: 0,
mtime: Date.now(),
@@ -216,7 +217,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,

View File

@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "0.0.1",
"version": "0.25.76-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
@@ -11,9 +11,16 @@
"run:docker": "docker run -p 8002:80 livesync-webapp",
"preview": "vite preview"
},
"dependencies": {},
"dependencies": {
"octagonal-wheels": "^0.1.46"
},
"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",
"vite": "^7.3.1"
"vite": "^8.0.16",
"vite-plugin-istanbul": "^9.0.1"
}
}

View File

@@ -7,7 +7,7 @@ import type { ServiceContext } from "@lib/services/base/ServiceBase";
import { FileAccessFSAPI } from "./FileAccessFSAPI";
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";

View File

@@ -1,5 +1,5 @@
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

View File

@@ -1,5 +1,5 @@
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

View File

@@ -12,6 +12,7 @@
import { LiveSyncWebApp } from "./main";
import type { ObsidianLiveSyncSettings } 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
@@ -41,7 +42,7 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
(core.services?.fileProcessing?.processing?.value ?? 0) +
(core.services?.replication?.storageApplyingCount?.value ?? 0);
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`);
}
@@ -116,7 +117,7 @@ export interface LiveSyncTestAPI {
const livesyncTest: LiveSyncTestAPI = {
async init(vaultName: string, settings: Partial<ObsidianLiveSyncSettings>): Promise<void> {
// Clean up any stale OPFS data from previous runs.
const opfsRoot = await navigator.storage.getDirectory();
const opfsRoot = await compatGlobal.navigator.storage.getDirectory();
try {
await opfsRoot.removeEntry(vaultName, { recursive: true });
} catch {
@@ -200,4 +201,4 @@ const livesyncTest: LiveSyncTestAPI = {
};
// Expose on window for Playwright page.evaluate() calls.
(window as any).livesyncTest = livesyncTest;
(compatGlobal as any).livesyncTest = livesyncTest;

View File

@@ -17,7 +17,7 @@
*/
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 path from "node:path";
import { fileURLToPath } from "node:url";

View File

@@ -21,7 +21,7 @@
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
// "baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]

View File

@@ -1,3 +1,5 @@
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
const HANDLE_DB_NAME = "livesync-webapp-handles";
const HANDLE_STORE_NAME = "handles";
const LAST_USED_KEY = "meta:lastUsedVaultId";
@@ -89,7 +91,7 @@ export class VaultHistoryStore {
async getVaultHistory(): Promise<VaultHistoryItem[]> {
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 items: VaultHistoryItem[] = [];
for (let i = 0; i < keys.length; i++) {
@@ -170,7 +172,7 @@ export class VaultHistoryStore {
}
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
const picker = (window as any).showDirectoryPicker;
const picker = (compatGlobal as any).showDirectoryPicker;
if (typeof picker !== "function") {
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
}

View File

@@ -13,13 +13,20 @@ This pseudo client actually receives the data from other devices, and sends if s
## 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
$ 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

View File

@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "0.0.0",
"version": "0.25.76-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,15 +11,17 @@
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"dependencies": {},
"dependencies": {
"octagonal-wheels": "^0.1.46"
},
"devDependencies": {
"eslint-plugin-svelte": "^3.15.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"eslint-plugin-svelte": "^3.19.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tsconfig/svelte": "^5.0.8",
"svelte": "5.41.1",
"svelte-check": "^443.3",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"typescript": "5.9.3",
"vite": "^7.3.1"
"vite": "^8.0.16"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",

View File

@@ -17,7 +17,9 @@ import {
type PeerStatus,
type PluginShim,
} 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 { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
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 { Menu } from "@lib/services/implements/browser/Menu";
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
function addToList(item: string, list: string) {
return unique(
@@ -137,7 +140,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
this._initP2PReplicator();
setTimeout(() => {
compatGlobal.setTimeout(() => {
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
void this.open();
}
@@ -164,12 +167,12 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
getConfig(key: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
return localStorage.getItem(dbKey);
return compatGlobal.localStorage.getItem(dbKey);
}
setConfig(key: string, value: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
localStorage.setItem(dbKey, value);
compatGlobal.localStorage.setItem(dbKey, value);
}
getDeviceName(): string {

View File

@@ -1,5 +1,5 @@
<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";
let result = $state<string | boolean>("");

View File

@@ -1,9 +1,10 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const app = mount(App, {
target: document.getElementById("app")!,
target: _activeDocument.getElementById("app")!,
});
export default app;

View File

@@ -1,9 +1,10 @@
import { mount } from "svelte";
import "./app.css";
import App from "./UITest.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const app = mount(App, {
target: document.getElementById("app")!,
target: _activeDocument.getElementById("app")!,
});
export default app;

View File

@@ -1,7 +1,7 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"sourceRoot": "../",
// "sourceRoot": "../",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
@@ -15,11 +15,13 @@
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
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 { Logger } from "octagonal-wheels/common/logger";
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};

View File

@@ -1,5 +1,5 @@
import { LOG_LEVEL_VERBOSE, Logger } from "@/lib/src/common/logger";
import type { KeyValueDatabase } from "@/lib/src/interfaces/KeyValueDatabase";
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase";
import { deleteDB, openDB, type IDBPDatabase } from "idb";
import { serialized } from "octagonal-wheels/concurrency/lock";

View File

@@ -1,4 +1,4 @@
import { eventHub } from "../lib/src/hub/hub";
import { eventHub } from "@lib/hub/hub";
// import type ObsidianLiveSyncPlugin from "../main";
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 };

View File

@@ -1,5 +1,5 @@
import type { TFile } from "../deps";
import type { FilePathWithPrefix, LoadedEntry } from "../lib/src/common/types";
import type { TFile } from "@/deps";
import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types";
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";

View File

@@ -36,7 +36,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
const r = await requestToCouchDBWithCredentials(
settings.couchDB_URI,
credential,
window.origin,
compatGlobal.origin,
undefined,
undefined,
undefined,

View File

@@ -1,6 +1,6 @@
import { type PluginManifest, TFile } from "../deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "../lib/src/common/types.ts";
export type { CacheData, FileEventItem } from "../lib/src/common/types.ts";
import { type PluginManifest, TFile } from "@/deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
export type { CacheData, FileEventItem } from "@lib/common/types.ts";
export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string;
@@ -51,7 +51,7 @@ export type queueItem = {
export const FileWatchEventQueueMax = 10;
export { configURIBase, configURIBaseQR } from "../lib/src/common/types.ts";
export { configURIBase, configURIBaseQR } from "@lib/common/types.ts";
export {
CHeader,
@@ -61,4 +61,4 @@ export {
ICHeaderEnd,
ICHeaderLength,
ICXHeader,
} from "../lib/src/common/models/fileaccess.const.ts";
} from "@lib/common/models/fileaccess.const.ts";

View File

@@ -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 {
path2id_base,
id2path_base,
@@ -7,9 +7,9 @@ import {
isValidFilenameInWidows,
isValidFilenameInAndroid,
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 {
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
@@ -22,14 +22,14 @@ import {
type FilePathWithPrefix,
type UXFileInfo,
type UXFileInfoStub,
} from "../lib/src/common/types.ts";
} from "@lib/common/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 { scheduleTask } from "octagonal-wheels/concurrency/task";
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
import { AuthorizationHeaderGenerator } from "@lib/replication/httplib.ts";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
@@ -132,7 +132,7 @@ export const _requestToCouchDBFetch = async (
method?: string
) => {
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str);
const encoded = compatGlobal.btoa(utf8str);
const authHeader = "Basic " + encoded;
const transformedHeaders: Record<string, string> = {
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 };
// Why 2000? : ZIP FILE Does not have enough resolution.
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 };
function getKey(file: AnyEntry | string | UXFileInfoStub) {
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);

View File

@@ -8,7 +8,7 @@ import {
diff_match_patch,
Platform,
addIcon,
} from "../../deps.ts";
} from "@/deps.ts";
import type {
EntryDoc,
@@ -19,7 +19,7 @@ import type {
AnyEntry,
SavingEntry,
diff_result,
} from "../../lib/src/common/types.ts";
} from "@lib/common/types.ts";
import {
CANCELLED,
LEAVE_TO_SUBSEQUENT,
@@ -29,8 +29,8 @@ import {
LOG_LEVEL_VERBOSE,
MODE_SELECTIVE,
MODE_SHINY,
} from "../../lib/src/common/types.ts";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "../../common/types.ts";
} from "@lib/common/types.ts";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "@/common/types.ts";
import {
createBlob,
createSavingEntryFromLoadedEntry,
@@ -42,12 +42,12 @@ import {
isDocContentSame,
isLoadedEntry,
isObjectDifferent,
} from "../../lib/src/common/utils.ts";
import { digestHash } from "../../lib/src/string_and_binary/hash.ts";
import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
} from "@lib/common/utils.ts";
import { digestHash } from "@lib/string_and_binary/hash.ts";
import { arrayBufferToBase64, decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
import { stripAllPrefixes } from "@lib/string_and_binary/path.ts";
import {
EVEN,
disposeMemoObject,
@@ -57,20 +57,20 @@ import {
memoObject,
retrieveMemoObject,
scheduleTask,
} from "../../common/utils.ts";
} from "@/common/utils.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 { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
import type ObsidianLiveSyncPlugin from "../../main.ts";
import { pluginScanningCount } from "@lib/mock_and_interop/stores.ts";
import type ObsidianLiveSyncPlugin from "@/main.ts";
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 { 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 { $msg } from "@/lib/src/common/i18n.ts";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
import { $msg } from "@lib/common/i18n.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
import { LiveSyncError } from "@lib/common/LSError.ts";
const d = "\u200b";

View File

@@ -5,10 +5,10 @@
type IPluginDataExDisplay,
type PluginDataExFile,
} from "./CmdConfigSync.ts";
import { Logger } from "../../lib/src/common/logger";
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
import type ObsidianLiveSyncPlugin from "../../main";
import { Logger } from "@lib/common/logger";
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types";
import { getDocData, timeDeltaToHumanReadable, unique } from "@lib/common/utils";
import type ObsidianLiveSyncPlugin from "@/main";
// import { askString } from "../../common/utils";
import { Menu } from "@/deps.ts";

View File

@@ -1,6 +1,6 @@
import { mount, unmount } from "svelte";
import { App, Modal } from "../../deps.ts";
import ObsidianLiveSyncPlugin from "../../main.ts";
import { App, Modal } from "@/deps.ts";
import ObsidianLiveSyncPlugin from "@/main.ts";
import PluginPane from "./PluginPane.svelte";
export class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
@@ -16,9 +16,11 @@ export class PluginDialogModal extends Modal {
override onOpen() {
const { contentEl } = this;
this.contentEl.style.overflow = "auto";
this.contentEl.style.display = "flex";
this.contentEl.style.flexDirection = "column";
this.contentEl.setCssStyles({
overflow: "auto",
display: "flex",
flexDirection: "column",
});
this.titleEl.setText("Customization Sync (Beta3)");
if (!this.component) {
this.component = mount(PluginPane, {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import ObsidianLiveSyncPlugin from "../../main";
import ObsidianLiveSyncPlugin from "@/main";
import {
ConfigSync,
type IPluginDataExDisplay,
@@ -11,16 +11,16 @@
} from "./CmdConfigSync.ts";
import PluginCombo from "./PluginCombo.svelte";
import { Menu, type PluginManifest } from "@/deps.ts";
import { unique } from "../../lib/src/common/utils";
import { unique } from "@lib/common/utils";
import {
MODE_SELECTIVE,
MODE_AUTOMATIC,
MODE_PAUSED,
type SYNC_MODE,
MODE_SHINY,
} from "../../lib/src/common/types";
import { normalizePath } from "../../deps";
import { HiddenFileSync } from "../HiddenFileSync/CmdHiddenFileSync.ts";
} from "@lib/common/types";
import { normalizePath } from "@/deps";
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
export let plugin: ObsidianLiveSyncPlugin;

View File

@@ -1,7 +1,7 @@
import { App, Modal } from "../../deps.ts";
import { type FilePath, type LoadedEntry } from "../../lib/src/common/types.ts";
import { App, Modal } from "@/deps.ts";
import { type FilePath, type LoadedEntry } from "@lib/common/types.ts";
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";
export class JsonResolveModal extends Modal {

View File

@@ -1,8 +1,8 @@
<script lang="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 { decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
import { getDocData, isObjectDifferent, mergeObject } from "../../lib/src/common/utils.ts";
import { type Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "@/deps.ts";
import type { FilePath, LoadedEntry } from "@lib/common/types.ts";
import { decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
import { getDocData, isObjectDifferent, mergeObject } from "@lib/common/utils.ts";
interface Props {
docs?: LoadedEntry[];

View File

@@ -1,4 +1,4 @@
import { type PluginManifest, type ListedFiles } from "../../deps.ts";
import { type PluginManifest, type ListedFiles } from "@/deps.ts";
import {
type LoadedEntry,
type FilePathWithPrefix,
@@ -15,8 +15,8 @@ import {
LOG_LEVEL_DEBUG,
type MetaEntry,
type UXDataWriteOptions,
} from "../../lib/src/common/types.ts";
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../../common/types.ts";
} from "@lib/common/types.ts";
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "@/common/types.ts";
import {
readAsBlob,
isDocContentSame,
@@ -26,7 +26,7 @@ import {
fireAndForget,
type CustomRegExp,
getFileRegExp,
} from "../../lib/src/common/utils.ts";
} from "@lib/common/utils.ts";
import {
compareMTime,
isInternalMetadata,
@@ -39,17 +39,17 @@ import {
BASE_IS_NEW,
EVEN,
displayRev,
} from "../../common/utils.ts";
} from "@/common/utils.ts";
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import { addPrefix, stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
import { addPrefix, stripAllPrefixes } from "@lib/string_and_binary/path.ts";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src/mock_and_interop/stores.ts";
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "@lib/mock_and_interop/stores.ts";
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
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";
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";

View File

@@ -7,12 +7,12 @@ import {
type FilePath,
type FilePathWithPrefix,
type LOG_LEVEL,
} from "../lib/src/common/types.ts";
import type ObsidianLiveSyncPlugin from "../main.ts";
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
import type { LiveSyncCore } from "../main.ts";
import { __$checkInstanceBinding } from "../lib/src/dev/checks.ts";
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
} from "@lib/common/types.ts";
import type ObsidianLiveSyncPlugin from "@/main.ts";
import { MARK_DONE } from "@/modules/features/ModuleLog.ts";
import type { LiveSyncCore } from "@/main.ts";
import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts";
let noticeIndex = 0;
export abstract class LiveSyncCommands {

View File

@@ -9,14 +9,14 @@ import {
type EntryLeaf,
type FilePathWithPrefix,
type MetaEntry,
} from "../../lib/src/common/types";
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
import { LiveSyncCommands } from "../LiveSyncCommands";
} from "@lib/common/types";
import { getNoFromRev } from "@lib/pouchdb/LiveSyncLocalDB";
import { LiveSyncCommands } from "@/features/LiveSyncCommands";
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
import { arrayToChunkedArray } from "octagonal-wheels/collection";
import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events";
import type { LiveSyncCouchDBReplicator } from "@/lib/src/replication/couchdb/LiveSyncReplicator";
import { delay } from "@/lib/src/common/utils";
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import { delay } from "@lib/common/utils";
// import { _requestToCouchDB } from "@/common/utils";
const DB_KEY_SEQ = "gc-seq";
const DB_KEY_CHUNK_SET = "chunk-set";

View File

@@ -1,7 +1,7 @@
import { App, Modal } from "@/deps.ts";
import P2POpenReplicationPane from "./P2POpenReplicationPane.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 = {
onSync: (peerId: string) => Promise<void>;

View File

@@ -9,7 +9,7 @@
// import type { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types";
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 P2PServerStatusCard from "./P2PServerStatusCard.svelte";

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { onMount, setContext } from "svelte";
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "../../../lib/src/common/types";
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "@lib/common/types";
import {
AcceptedStatus,
ConnectionStatus,
type PeerStatus,
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte";
import PeerStatusRow from "@/features/P2PSync/P2PReplicator/PeerStatusRow.svelte";
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
import {
type PeerInfo,

View File

@@ -9,7 +9,7 @@ import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "@lib/common/types.ts";
import { Logger } from "@lib/common/logger.ts";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU, type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon.ts";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult";
import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult";
export const VIEW_TYPE_P2P = "p2p-replicator";
function addToList(item: string, list: string) {

View File

@@ -9,9 +9,9 @@
EVENT_P2P_REPLICATOR_STATUS,
} from "@lib/replication/trystero/TrysteroReplicatorP2PServer";
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
import type { P2PReplicatorStatus } from "@/lib/src/replication/trystero/TrysteroReplicator";
import { extractP2PRoomSuffix } from "@/lib/src/common/utils";
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import type { P2PReplicatorStatus } from "@lib/replication/trystero/TrysteroReplicator";
import { extractP2PRoomSuffix } from "@lib/common/utils";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
interface Props {

View File

@@ -2,7 +2,7 @@ import { WorkspaceLeaf } from "@/deps.ts";
import { mount } from "svelte";
import { SvelteItemView } from "@/common/SvelteItemView.ts";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult";
import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult";
import P2PServerStatusPane from "./P2PServerStatusPane.svelte";
export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status";

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { getContext } from "svelte";
import { AcceptedStatus, type PeerStatus } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
import type { LiveSyncTrysteroReplicator } from "../../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
import { eventHub } from "../../../common/events";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
import { AcceptedStatus, type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { eventHub } from "@/common/events";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "@lib/replication/trystero/P2PReplicatorPaneCommon";
interface Props {
peerStatus: PeerStatus;

Submodule src/lib updated: 53804cbaec...c926417f82

View File

@@ -3,7 +3,7 @@ import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
export abstract class AbstractModule<
T extends LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands> = LiveSyncBaseCore<

View File

@@ -1,6 +1,6 @@
import { type Prettify } from "../lib/src/common/types";
import type { LiveSyncCore } from "../main";
import type ObsidianLiveSyncPlugin from "../main";
import { type Prettify } from "@lib/common/types";
import type { LiveSyncCore } from "@/main";
import type ObsidianLiveSyncPlugin from "@/main";
import { AbstractModule } from "./AbstractModule.ts";
import type { ChainableExecuteFunction, OverridableFunctionsKeys } from "./ModuleTypes";

View File

@@ -1,5 +1,5 @@
import type { Prettify } from "../lib/src/common/types";
import type { LiveSyncCore } from "../main";
import type { Prettify } from "@lib/common/types";
import type { LiveSyncCore } from "@/main";
export type OverridableFunctionsKeys<T> = {
[K in keyof T as K extends `$${string}` ? K : never]: T[K];

View File

@@ -1,6 +1,6 @@
import { PeriodicProcessor } from "@/common/PeriodicProcessor";
import type { LiveSyncCore } from "../../main";
import { AbstractModule } from "../AbstractModule";
import type { LiveSyncCore } from "@/main";
import { AbstractModule } from "@/modules/AbstractModule";
export class ModulePeriodicProcess extends AbstractModule {
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.services.replication.replicate());

View File

@@ -1,18 +1,18 @@
import type PouchDB from "pouchdb-core";
import { fireAndForget } from "octagonal-wheels/promises";
import { AbstractModule } from "../AbstractModule";
import { AbstractModule } from "@/modules/AbstractModule";
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "@lib/common/types";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "@/common/events";
import { $msg } from "../../lib/src/common/i18n";
import type { LiveSyncCore } from "../../main";
import { $msg } from "@lib/common/i18n";
import type { LiveSyncCore } from "@/main";
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
import { clearHandlers } from "@lib/replication/SyncParamsHandler";

View File

@@ -1,9 +1,9 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "../AbstractModule";
import type { LiveSyncCore } from "../../main";
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "@lib/common/types";
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "@/modules/AbstractModule";
import type { LiveSyncCore } from "@/main";
export class ModuleReplicatorCouchDB extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {

View File

@@ -1,8 +1,8 @@
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
import { LiveSyncJournalReplicator } from "../../lib/src/replication/journal/LiveSyncJournalReplicator";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import type { LiveSyncCore } from "../../main";
import { AbstractModule } from "../AbstractModule";
import { REMOTE_MINIO, type RemoteDBSettings } from "@lib/common/types";
import { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
import type { LiveSyncCore } from "@/main";
import { AbstractModule } from "@/modules/AbstractModule";
export class ModuleReplicatorMinIO extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {

View File

@@ -8,7 +8,7 @@ import {
type MetaEntry,
} from "@lib/common/types";
import type { ModuleReplicator } from "./ModuleReplicator";
import { isChunk } from "@/lib/src/common/typeUtils";
import { isChunk } from "@lib/common/typeUtils";
import {
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,

View File

@@ -1,9 +1,9 @@
import { AbstractModule } from "../AbstractModule.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "@lib/common/types";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { sendValue } from "octagonal-wheels/messagepassing/signal";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
export class ModuleConflictChecker extends AbstractModule {
async _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {

View File

@@ -1,5 +1,5 @@
import { serialized } from "octagonal-wheels/concurrency/lock";
import { AbstractModule } from "../AbstractModule.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import {
AUTO_MERGED,
CANCELLED,
@@ -10,15 +10,15 @@ import {
NOT_CONFLICTED,
type diff_check_result,
type FilePathWithPrefix,
} from "../../lib/src/common/types";
} from "@lib/common/types";
import { isCustomisationSyncMetadata, isPluginMetadata } from "@lib/common/typeUtils.ts";
import { TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
import { compareMTime, displayRev } from "@lib/common/utils.ts";
import diff_match_patch from "diff-match-patch";
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
import { eventHub } from "../../common/events.ts";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
import { stripAllPrefixes, isPlainText } from "@lib/string_and_binary/path";
import { eventHub } from "@/common/events.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
declare global {
interface LSEvents {
@@ -182,9 +182,9 @@ export class ModuleConflictResolver extends AbstractModule {
revs.map(async (rev) => {
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
if (leaf == false) {
return [0, rev] as [number, string];
return [0, rev];
}
return [leaf.mtime, rev] as [number, string];
return [leaf.mtime, rev];
})
)),
] as [number, string][]

View File

@@ -10,12 +10,12 @@ import {
type RemoteDBSettings,
IncompatibleChangesInSpecificPattern,
CompatibleButLossyChanges,
} from "../../lib/src/common/types.ts";
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
import { AbstractModule } from "../AbstractModule.ts";
import { $msg } from "../../lib/src/common/i18n.ts";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
} from "@lib/common/types.ts";
import { escapeMarkdownValue } from "@lib/common/utils.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import { $msg } from "@lib/common/i18n.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
function valueToString(value: any) {

Some files were not shown because too many files have changed in this diff Show More