Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@
"eslint/no-loop-func": "off",
"eslint/radix": "off",
"unicorn/import-style": "off",
"unicorn/no-process-exit": "off"
"unicorn/no-process-exit": "off",
"eslint/one-var": ["warn", "never"]
},
"env": {
"builtin": true,
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ This is the monorepo for an extensible Obsidian syncing plugin to sync vault fil
- Excluding main plugin, shared utils and documentation site, all packages are Sync Engine modules, they use the SDK and follow unified module structure.
- `null` forbidden, use `undefined` consistently.
- Lint warnings must be cleared, except time-bounded ones (TODO with date, deprecated API for compat)
- SDK types (`**/*.d.ts` in `packages/plugin/dist/`) are committed to satisfy Obsidian automated linting. You must not touch these types.

## Documentation

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Sync Engine fits the gap: you want to choose your own storage, you want the plug

### Core Functions

- Bidirectional syncing.
- Bidirectional / mirror remote / mirror local syncing.
- Startup / periodic / save-on-change syncing.
- Conflict resolution strategies (keep both / latest survive / keep remote / keep local / skip).
- Rate / memory control options.
Expand Down Expand Up @@ -138,6 +138,7 @@ Sync Engine also has a [wishlist of features](https://github.com/hesprs/sync-eng

- [x] v3.0: Rewrite entirely, dynamic module loading, module store, asymmetric storage, and rebrand
- [ ] v3.1: Migrate settings to Obsidian v1.13 API
- [ ] v3.2: Granular sync strategy selection / exclusion inclusion rule refactor based on ordered glob match rules.

## License

Expand Down
3 changes: 2 additions & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Sync Engine 恰好填补了这一空白:您可以自由选择存储服务,

### 核心功能

- 双向同步
- 双向 / 镜像远程 / 镜像本地同步
- 启动同步 / 定时同步 / 改动时保存同步。
- 冲突解决策略(保留两者 / 最新优先 / 保留远程 / 保留本地 / 跳过)。
- 速率 / 内存控制选项。
Expand Down Expand Up @@ -131,6 +131,7 @@ Sync Engine 还包含一份[期望功能清单](https://github.com/hesprs/sync-e

- [x] v3.0:全面重构,支持动态模块加载、模块商店、非对称存储,并完成品牌重塑
- [ ] v3.1:将设置项迁移至 Obsidian v1.13 API
- [ ] v3.2:精细化同步策略选择 / 重构包含与排除规则基于有序 Glob 匹配规则

## 开源协议与版权

Expand Down
118 changes: 61 additions & 57 deletions bun.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion docs/src/pages/en/deep-dive/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ If the remote root does not exist, the full lister recreates it, clears records

## Decider

The selected decider receives filtered local stats, filtered remote stats, persistent records, a task factory, and a logger. The built-in bidirectional decider unions all keys found in either side or in the records. The selected [sync strategy](../usage/settings#sync-strategy) can be supplied by a module.
The selected decider receives filtered local stats, filtered remote stats, persistent records, a task factory, and a logger. Built-in deciders union all keys found in either side or in the records. The selected [sync strategy](../usage/settings#sync-strategy) can be supplied by a module.

**Bidirectional sync strategy**:

For files, it compares current stats with recorded local and remote UIDs. It creates upload, download, local removal, remote removal, record, or conflict tasks according to which side exists and changed. When both sides exist without a record, equal-size files only create a record; unequal-size files become conflicts. Missing entries on both sides produce record-removal tasks.

Folders produce directory creation, removal, or record tasks. A local/remote file-folder mismatch replaces the changed side when it can be determined; an unresolvable mismatch fails planning.

**Mirror local / Mirror remote sync strategy**:

These two deciders make one side authoritative. They copy authoritative files, create authoritative folders, remove entries present only on the other side, and replace file-folder mismatches. A matching record avoids a redundant file transfer. Unrecorded files with matching keys and sizes receive a record; other unrecorded files are copied from the authoritative side.

## Move Detection

Move detection runs after the decider. It pairs a delete task with a create task on the same side when their recorded/current file UIDs match, then replaces the pair with `moveLocal` or `moveRemote`.
Expand Down
4 changes: 2 additions & 2 deletions docs/src/pages/en/development/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ type DeciderInput = {
type Decider = (input: DeciderInput) => Array<BaseTask>;
```

Use `taskFactory` instead of constructing task classes directly — their constructors require internal sync infrastructure. For the built-in bidirectional decider logic, see [deep dive: sync](../deep-dive/sync#decider).
Use `taskFactory` instead of constructing task classes directly — their constructors require internal sync infrastructure. Built-in deciders include bidirectional, mirror-local, and mirror-remote; see [deep dive: sync](../deep-dive/sync#decider) for their behavior.

Example: [bidirectional decider](https://github.com/hesprs/sync-engine/blob/main/packages/plugin/src/sync/decision/bidirectional.ts).
Examples: [bidirectional decider](https://github.com/hesprs/sync-engine/blob/main/packages/plugin/src/sync/decision/bidirectional.ts) and [mirror deciders](https://github.com/hesprs/sync-engine/blob/main/packages/plugin/src/sync/decision/mirror.ts).

### Registering a Decider

Expand Down
8 changes: 7 additions & 1 deletion docs/src/pages/en/usage/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ Automatically update installed modules from their configured sources. Enabled by

### Sync Strategy

Choose how Sync Engine decides what to do when local and remote files differ. The default **Bidirectional** strategy can apply changes in both local and remote side. Other strategies may be supplied by modules.
Choose how Sync Engine decides what to do when local and remote files differ:

- **Bidirectional** is the default. It can apply changes on both sides and asks the selected conflict resolver to handle simultaneous file changes.
- **Mirror local** makes local vault authoritative. It copies local entries to remote and removes remote-only entries.
- **Mirror remote** makes remote storage authoritative. It copies remote entries to local and removes local-only entries.

Mirror strategies overwrite changes on the non-authoritative side without conflict resolution. Other strategies may be supplied by modules.

### Conflict Resolve Strategy

Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/en/usage/why-sync-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Sync Engine core offers necessary features to ensure the extensibility and perfo

### Core Functions

- Bidirectional syncing.
- Bidirectional / mirror local / mirror-remote syncing.
- Startup / periodic / save-on-change syncing.
- Conflict resolution strategies (keep both / latest survive / keep remote / keep local / skip).
- Rate / memory control options.
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "sync-engine",
"name": "Sync Engine",
"version": "3.0.5",
"version": "3.0.6",
"minAppVersion": "1.12.3",
"authorUrl": "https://hesprs.github.io",
"description": "The next-generation syncing plugin: Fast · Free · Extend with Modules. Supports WebDAV and S3.",
Expand Down
6 changes: 3 additions & 3 deletions modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
{
"id": "i18n-zh",
"name": "I18n 简体中文",
"version": "0.0.11",
"version": "0.0.12",
"description": "Simplified Chinese UI language pack. / 简体中文介面语言包。",
"icon": "languages",
"main": "https://sync.consensia.cc/modules/i18n-zh.js",
Expand All @@ -38,7 +38,7 @@
{
"id": "i18n-zh-TW",
"name": "I18n 繁體中文",
"version": "0.0.3",
"version": "0.0.4",
"description": "Traditional Chinese UI language pack. / 繁體中文介面語言包。",
"icon": "languages",
"main": "https://sync.consensia.cc/modules/i18n-zh-TW.js",
Expand All @@ -47,7 +47,7 @@
{
"id": "i18n-ru",
"name": "I18n Русский",
"version": "0.0.3",
"version": "0.0.4",
"description": "Russian UI language pack. / Пакет русского интерфейса.",
"icon": "languages",
"main": "https://sync.consensia.cc/modules/i18n-ru.js",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
"@tsdown/css": "^0.22.14",
"@types/bun": "^1.3.14",
"obsidian": "^1.13.1",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"oxlint-tsgolint": "^7.0.2001",
"tsdown": "^0.22.14",
"turbo": "^2.10.8",
"turbo": "^2.10.9",
"typescript": "^7.0.2"
},
"packageManager": "bun@1.3.13"
Expand Down
2 changes: 1 addition & 1 deletion packages/encryption/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"devDependencies": {
"@hesprs/sync-engine-sdk": "workspace:*",
"@noble/ciphers": "^2.2.0",
"@noble/ciphers": "^2.3.0",
"@repo/shared": "workspace:*",
"hash-wasm": "^4.12.0",
"uni-kv": "../../uni-kv.tgz"
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/ru/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ const ru: Translations = {
minRequestIntervalDescription:
'Ограничить минимальное время между последовательными запросами во время синхронизации. Полезно для сервисов с ограничением частоты запросов. Измените интервал в поле ниже.',
minRequestIntervalPlaceholder: 'Введите интервал (например, 1s, 500ms)',
mirrorLocal: 'Зеркало локального хранилища',
mirrorRemote: 'Зеркало удалённого хранилища',
miscellaneous: 'Разное',
moduleAutoUpdate: 'Автообновление модулей',
moduleAutoUpdateDescription: 'Автоматически обновлять установленные модули из их источников.',
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/zh-TW/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ const zhTW: Translations = {
minRequestIntervalDescription:
'限制同步過程中連續請求之間的最小時間間隔。此選項適用於有請求速率限制的服務。請在欄位中修改間隔時間。',
minRequestIntervalPlaceholder: '輸入間隔時間(例如 1s, 500ms)',
mirrorLocal: '鏡像本機',
mirrorRemote: '鏡像遠端',
miscellaneous: '雜項設定',
moduleAutoUpdate: '自動更新模組',
moduleAutoUpdateDescription: '自動從模組來源更新已安裝的模組。',
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/zh/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ const zh: Translations = {
minRequestIntervalDescription:
'限制同步过程中连续请求之间的最小时间间隔。此选项对于有请求频率限制的服务非常有用。在输入框中修改间隔。',
minRequestIntervalPlaceholder: '输入间隔(例如 1s, 500ms)',
mirrorLocal: '镜像本地',
mirrorRemote: '镜像远程',
miscellaneous: '杂项',
moduleAutoUpdate: '自动更新模块',
moduleAutoUpdateDescription: '从模块源自动更新已安装的模块。',
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## Sync Engine v3.0.6 - 2026-08-14

- Added **Mirror remote** and **Mirror local** sync strategies.

## Sync Engine v3.0.5 - 2026-08-12

- Fixed the bug that non-root hidden files and folders cannot be discovered in syncing by @pedrovillalobos.
Expand Down
24 changes: 23 additions & 1 deletion packages/plugin/dist/dev.spec.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ct as FolderStat, St as FileStat, f as Request, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-Ddt5UAXp.spec.js";
import { Ct as FolderStat, Dt as RecordStatsMap, Et as RecordStat, J as TaskNames, M as Decider, Ot as Stat, St as FileStat, f as Request, kt as StatsMap, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-BJDjAUwk.spec.js";
//#region src/sdk/debug-wrapper.d.ts
declare function debugWrapper(original: Fs, log: (content: string) => void): WrappedFs;
//#endregion
Expand Down Expand Up @@ -28,13 +28,29 @@ type RequestHarness = {
calls: Array<RequestParam | string>;
request: Request;
};
type ExtractedTask = {
key: string;
local?: Stat;
name: TaskNames;
remote?: Stat;
};
declare function bytes(value: string): Binary;
declare function file(key: string, options?: {
mtime?: number;
size?: number;
uid?: string;
}): FileStat;
declare function folder(key: string): FolderStat;
declare function fileRecord(local: string, remote: string): RecordStat;
declare function folderRecord(): RecordStat;
declare function runDecider(decider: Decider, input: {
localStats?: StatsMap;
remoteStats?: StatsMap;
records?: RecordStatsMap;
}): Array<ExtractedTask>;
declare function taskNames(tasks: Array<ExtractedTask>): Array<string>;
declare function taskKeys(tasks: Array<ExtractedTask>): Array<string>;
declare function findTask(tasks: Array<ExtractedTask>, key: string): ExtractedTask;
declare function stream(chunks?: Array<string | Binary>): ReadableStream<Binary>;
declare function deferred<T>(): {
promise: Promise<T>;
Expand All @@ -48,11 +64,17 @@ declare const testKit: {
bytes: typeof bytes;
deferred: typeof deferred;
file: typeof file;
fileRecord: typeof fileRecord;
findTask: typeof findTask;
flush: typeof flush;
folder: typeof folder;
folderRecord: typeof folderRecord;
fs: typeof fs;
request: typeof request;
runDecider: typeof runDecider;
stream: typeof stream;
taskKeys: typeof taskKeys;
taskNames: typeof taskNames;
};
//#endregion
//#region src/utils/sha-256.d.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,8 @@ declare class Bootstrap {
private remoteFs?;
readonly i18n: {
bidirectional: string;
mirrorLocal: string;
mirrorRemote: string;
latestSurvive: string;
keepLocal: string;
keepRemote: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/dist/index.spec.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-Ddt5UAXp.spec.js";
import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-BJDjAUwk.spec.js";
export { type AddRecord, type AugmentedModuleMeta, type BaseTask, type BatchOptimizer, type Binary, type CheckConnectionResult, type ConflictResolver, type ConflictResolverEntry, type ConflictResolverPayload, type Context, type CreateLocalDir, type CreateRemoteDir, type CustomAtom, type DatabaseAsync, type DatabaseSync, type Decider, type DeciderEntry, type DeciderInput, type DeleteAtom, type Dispatch, type Download, type Events, type ExistingMemoryDB, type FileStat, type FolderStat, type Fragment, type Fs, type FsWrapperEntry, type InputAtom, type ListReporter, type LocalRequestMiddlewareEntry, type MaybePromise, type MkdirAtom, type ModuleMeta, type MoveAtom, type MoveLocal, type MoveRemote, type ObsidianLanguageCode, type On, type OptimizerEntry, type OptimizerInput, type OptimizerOutput, type OutputAtom, type Progress, type RecordStat, type RecordStatsMap, type RecordStore, type RemoteFsEntry, type RemoteLister, type RemoteListerEntry, type RemoteRequestMiddlewareEntry, type RemoveLocal, type RemoveRecord, type RemoveRemote, type Request, type RequestParam, type RequestResponse, type ResolveConflict, type RootFs, SelectFromContext, type SettingEntry, type Settings, type Stat, type StatsMap, type StoreAsync, type StoreOperations, type StoreSync, type SyncTerminateReason, type TaskFactory, type TaskNames, type Translate, type TranslationResource, type Translations, type Upload, type VaultRequest, type WrappedFs, type WriteAtom, digOriginal, pipe, prefixWrapper, readWithSize, setNeedMigration, writeWithValue };
4 changes: 2 additions & 2 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hesprs/sync-engine-sdk",
"version": "3.0.5",
"version": "3.0.6",
"description": "Official SDK for developing modules targeting Sync Engine, the extensible Obsidian syncing plugin.",
"keywords": [
"obsidian-plugin",
Expand Down Expand Up @@ -56,7 +56,7 @@
"devDependencies": {
"@repo/shared": "workspace:*",
"@unocss/postcss": "^66.7.5",
"postcss-merge-rules": "^8.0.2",
"postcss-merge-rules": "^8.0.3",
"solid-js": "^1.9.13",
"synthkernel": "./synthkernel.tgz",
"uni-kv": "../../uni-kv.tgz",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ const en: Translations = {
minRequestIntervalDescription:
'Limit the minimum time between consecutive requests during synchronization. This option is useful for services with request rate limits. Alter the interval in the field.',
minRequestIntervalPlaceholder: 'Enter interval (e.g. 1s, 500ms)',
mirrorLocal: 'Mirror local',
mirrorRemote: 'Mirror remote',
miscellaneous: 'Miscellaneous',
moduleAutoUpdate: 'Auto-update modules',
moduleAutoUpdateDescription: 'Automatically update installed modules from module sources.',
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin/src/fs/wrappers/asymmetric-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ export default function asymmetricStorageWrapper(
const SAFE_81 = " !$'(),-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}";

function generateId(str: string): string {
let h1 = Math.trunc(0xde_ad_be_ef),
h2 = Math.trunc(0x41_c6_ce_57);
let h1 = Math.trunc(0xde_ad_be_ef);
let h2 = Math.trunc(0x41_c6_ce_57);
for (const char of str) {
const ch = char.codePointAt(0) as number;
h1 = Math.imul(h1 ^ ch, 2_654_435_761);
Expand Down
Loading
Loading