diff --git a/.oxlintrc.json b/.oxlintrc.json index 0e90b69..d8e80d0 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -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, diff --git a/AGENTS.md b/AGENTS.md index fb31a97..a108d19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index 09b25d6..7e58608 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/README.zh.md b/README.zh.md index dbd28e7..ff8b2bc 100644 --- a/README.zh.md +++ b/README.zh.md @@ -63,7 +63,7 @@ Sync Engine 恰好填补了这一空白:您可以自由选择存储服务, ### 核心功能 -- 双向同步。 +- 双向 / 镜像远程 / 镜像本地同步。 - 启动同步 / 定时同步 / 改动时保存同步。 - 冲突解决策略(保留两者 / 最新优先 / 保留远程 / 保留本地 / 跳过)。 - 速率 / 内存控制选项。 @@ -131,6 +131,7 @@ Sync Engine 还包含一份[期望功能清单](https://github.com/hesprs/sync-e - [x] v3.0:全面重构,支持动态模块加载、模块商店、非对称存储,并完成品牌重塑 - [ ] v3.1:将设置项迁移至 Obsidian v1.13 API +- [ ] v3.2:精细化同步策略选择 / 重构包含与排除规则基于有序 Glob 匹配规则 ## 开源协议与版权 diff --git a/bun.lock b/bun.lock index 6269224..4241b7a 100644 --- a/bun.lock +++ b/bun.lock @@ -8,11 +8,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", }, }, @@ -36,7 +36,7 @@ "version": "0.0.1", "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", @@ -51,14 +51,14 @@ }, "packages/plugin": { "name": "@hesprs/sync-engine-sdk", - "version": "3.0.2", + "version": "3.0.5", "dependencies": { "obsidian": "^1.13.1", }, "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", @@ -181,7 +181,7 @@ "@needle-di/core": ["@needle-di/core@1.1.3", "", {}, "sha512-Jeals0rTBlC9rP0LjgEX3xaD9FCZykCMmiCBxvjaL6UjNRyd8fpp64kFgQi1IYt/6aeNOrjm4ElK9PdIlPKqLQ=="], - "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + "@noble/ciphers": ["@noble/ciphers@2.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.131.0", "", { "os": "android", "cpu": "arm" }, "sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg=="], @@ -225,43 +225,43 @@ "@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.62.0", "", { "os": "android", "cpu": "arm" }, "sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A=="], - "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.62.0", "", { "os": "android", "cpu": "arm64" }, "sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ=="], - "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.62.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A=="], - "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.62.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA=="], - "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.62.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg=="], - "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.62.0", "", { "os": "linux", "cpu": "arm" }, "sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg=="], - "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.62.0", "", { "os": "linux", "cpu": "arm" }, "sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw=="], - "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.62.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw=="], - "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.62.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg=="], - "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.62.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg=="], - "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.62.0", "", { "os": "linux", "cpu": "none" }, "sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw=="], - "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.62.0", "", { "os": "linux", "cpu": "none" }, "sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A=="], + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg=="], - "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.62.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A=="], + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA=="], - "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.62.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w=="], + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA=="], - "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.62.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw=="], + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ=="], - "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.62.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA=="], + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw=="], - "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.62.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw=="], + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g=="], - "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.62.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw=="], + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g=="], - "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.62.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ=="], + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ=="], "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], @@ -275,43 +275,43 @@ "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.77.0", "", { "os": "android", "cpu": "arm" }, "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.77.0", "", { "os": "android", "cpu": "arm64" }, "sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.77.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.77.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.77.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.77.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.77.0", "", { "os": "linux", "cpu": "arm" }, "sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.77.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.77.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.77.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.78.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.77.0", "", { "os": "linux", "cpu": "none" }, "sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.77.0", "", { "os": "linux", "cpu": "none" }, "sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.77.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.78.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.77.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.77.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.77.0", "", { "os": "none", "cpu": "arm64" }, "sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.77.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.77.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.77.0", "", { "os": "win32", "cpu": "x64" }, "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -373,17 +373,17 @@ "@tsdown/css": ["@tsdown/css@0.22.14", "", { "dependencies": { "lightningcss": "^1.33.0", "postcss-load-config": "^6.0.1", "rolldown": "~1.2.0" }, "peerDependencies": { "postcss": "^8.4.0", "postcss-import": "^16.0.0", "postcss-modules": "^6.0.0", "sass": "*", "sass-embedded": "*", "tsdown": "0.22.14" }, "optionalPeers": ["postcss", "postcss-import", "postcss-modules", "sass", "sass-embedded"] }, "sha512-0NpbgqfhfjaAoLdY3DxjrMo08vKFEtvlybCS42Gtolx8OPoKUdZZDyls0qgNEqea0c7XGjlF0NIE1tIxgU8RqA=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.10.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.10.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jh+pTGXLNz8+1tkUU13TI/f+ZOI+OvC4YbHi1H+57iSpLt5DR3xgptd+4sA07RdjdRR/RX/01uQQu2OkbzIefA=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+zB2btDJ00lnPRuqOvpVvgl4x34k/djZQGZTTCfjn7JgNCl8QFY5Njo5+dqkY1g/+9gbbsnAvWm9CmJg9ebcXA=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aqtpPkiIC4IUas8Vv27oJ3aDfTuP1d5wofd01dZ7gfhHRrIKuFdqQb4imvNnVFahcOViF9Jh8Oi7feDoz8/ciA=="], - "@turbo/linux-64": ["@turbo/linux-64@2.10.8", "", { "os": [ "linux", "android", ], "cpu": "x64" }, "sha512-K1dxqiVisyN7cViVsfQLs6xscQbYuI8aO2nbUhFURDACgEDfZRdP/b4CCxeosBJpcMfhYyiibWqJorCnvz9kKg=="], + "@turbo/linux-64": ["@turbo/linux-64@2.10.9", "", { "os": [ "linux", "android", ], "cpu": "x64" }, "sha512-XyAneUBsS5uNOUOjBSs81zyigMVwwhVUd3u7F2JFMKGQk6F7eNAiOEBASJ+aHLldjYsOEjhfW+5gWIqwziyFFw=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.8", "", { "os": [ "linux", "android", ], "cpu": "arm64" }, "sha512-Gi77ibVnrE1fEmvr+/wBD/yvRqhwp/RQuCp2+//lv1U1wNFFyVg0V7Wj8FG9FXPFAw5QHReo8rxc9+wBSDZjzA=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.9", "", { "os": [ "linux", "android", ], "cpu": "arm64" }, "sha512-5jAcldLnkuWIjujGhCn2MGIeUwW8IVNOq9Sce4EEzzgLcxmhTasbV0RiW6ZkaRdOjmOCGzeNXPLkDJEOIucOLw=="], - "@turbo/windows-64": ["@turbo/windows-64@2.10.8", "", { "os": "win32", "cpu": "x64" }, "sha512-znnLO1haJPYTHoKMKwlAvlkjRiYbbhBzME6wIGaMd+fwir23U6jVd1ecaTWWi1fbnRVqxMfgDBKseQ/hLKb83g=="], + "@turbo/windows-64": ["@turbo/windows-64@2.10.9", "", { "os": "win32", "cpu": "x64" }, "sha512-u1xGpGlefzuhBedbt/VR2nWdftfFVZwPeDg9e5uZl68sV1fL3HNYTNr5VyHkzgtPK2VTYg1x4OKZuGrh1Gvhtg=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-VN30vh3b3Czh2WzYHNTfF1FE0YMZ5aHsLO8dBMGHJewA6792wX6iJR8ZxlzFW6WdOu0gEAKIvlYhfyT81Wkm4Q=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-W2Ub165Qv0iMFljbYKxxbZN+2dVSngnzEjQNEFXtnz5oJVx9XSypmNmUvMtuYMeZ3kdVC1zI7yd/rtuX+SDnbg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], @@ -605,7 +605,7 @@ "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], - "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -639,7 +639,7 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "cssnano-utils": ["cssnano-utils@6.0.2", "", { "peerDependencies": { "postcss": "^8.5.25" } }, "sha512-AqysikWP69dOKAP/UMvUYrlZ1gvvQu0/eMFVUKLhH+ZM23dUAKXC31xKvNyL9It2UMF2uDmK4XotBeO9vSBKzg=="], + "cssnano-utils": ["cssnano-utils@6.0.3", "", { "peerDependencies": { "postcss": "^8.5.26" } }, "sha512-HskzChO3gRkXBQSWg68DfwoVfptRUV2GvuiQBvt+C7mUw4VB0CvPjkC4iF4JSf7yW1/66Hsc6WJtqNqD5Ydt2Q=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -803,9 +803,9 @@ "oxc-walker": ["oxc-walker@0.7.0", "", { "dependencies": { "magic-regexp": "^0.10.0" }, "peerDependencies": { "oxc-parser": ">=0.98.0" } }, "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A=="], - "oxfmt": ["oxfmt@0.62.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.62.0", "@oxfmt/binding-android-arm64": "0.62.0", "@oxfmt/binding-darwin-arm64": "0.62.0", "@oxfmt/binding-darwin-x64": "0.62.0", "@oxfmt/binding-freebsd-x64": "0.62.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.62.0", "@oxfmt/binding-linux-arm-musleabihf": "0.62.0", "@oxfmt/binding-linux-arm64-gnu": "0.62.0", "@oxfmt/binding-linux-arm64-musl": "0.62.0", "@oxfmt/binding-linux-ppc64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-musl": "0.62.0", "@oxfmt/binding-linux-s390x-gnu": "0.62.0", "@oxfmt/binding-linux-x64-gnu": "0.62.0", "@oxfmt/binding-linux-x64-musl": "0.62.0", "@oxfmt/binding-openharmony-arm64": "0.62.0", "@oxfmt/binding-win32-arm64-msvc": "0.62.0", "@oxfmt/binding-win32-ia32-msvc": "0.62.0", "@oxfmt/binding-win32-x64-msvc": "0.62.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ=="], + "oxfmt": ["oxfmt@0.63.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.63.0", "@oxfmt/binding-android-arm64": "0.63.0", "@oxfmt/binding-darwin-arm64": "0.63.0", "@oxfmt/binding-darwin-x64": "0.63.0", "@oxfmt/binding-freebsd-x64": "0.63.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", "@oxfmt/binding-linux-arm64-gnu": "0.63.0", "@oxfmt/binding-linux-arm64-musl": "0.63.0", "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", "@oxfmt/binding-linux-riscv64-musl": "0.63.0", "@oxfmt/binding-linux-s390x-gnu": "0.63.0", "@oxfmt/binding-linux-x64-gnu": "0.63.0", "@oxfmt/binding-linux-x64-musl": "0.63.0", "@oxfmt/binding-openharmony-arm64": "0.63.0", "@oxfmt/binding-win32-arm64-msvc": "0.63.0", "@oxfmt/binding-win32-ia32-msvc": "0.63.0", "@oxfmt/binding-win32-x64-msvc": "0.63.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw=="], - "oxlint": ["oxlint@1.77.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.77.0", "@oxlint/binding-android-arm64": "1.77.0", "@oxlint/binding-darwin-arm64": "1.77.0", "@oxlint/binding-darwin-x64": "1.77.0", "@oxlint/binding-freebsd-x64": "1.77.0", "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", "@oxlint/binding-linux-arm-musleabihf": "1.77.0", "@oxlint/binding-linux-arm64-gnu": "1.77.0", "@oxlint/binding-linux-arm64-musl": "1.77.0", "@oxlint/binding-linux-ppc64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-musl": "1.77.0", "@oxlint/binding-linux-s390x-gnu": "1.77.0", "@oxlint/binding-linux-x64-gnu": "1.77.0", "@oxlint/binding-linux-x64-musl": "1.77.0", "@oxlint/binding-openharmony-arm64": "1.77.0", "@oxlint/binding-win32-arm64-msvc": "1.77.0", "@oxlint/binding-win32-ia32-msvc": "1.77.0", "@oxlint/binding-win32-x64-msvc": "1.77.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg=="], + "oxlint": ["oxlint@1.78.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.78.0", "@oxlint/binding-android-arm64": "1.78.0", "@oxlint/binding-darwin-arm64": "1.78.0", "@oxlint/binding-darwin-x64": "1.78.0", "@oxlint/binding-freebsd-x64": "1.78.0", "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", "@oxlint/binding-linux-arm-musleabihf": "1.78.0", "@oxlint/binding-linux-arm64-gnu": "1.78.0", "@oxlint/binding-linux-arm64-musl": "1.78.0", "@oxlint/binding-linux-ppc64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-musl": "1.78.0", "@oxlint/binding-linux-s390x-gnu": "1.78.0", "@oxlint/binding-linux-x64-gnu": "1.78.0", "@oxlint/binding-linux-x64-musl": "1.78.0", "@oxlint/binding-openharmony-arm64": "1.78.0", "@oxlint/binding-win32-arm64-msvc": "1.78.0", "@oxlint/binding-win32-ia32-msvc": "1.78.0", "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA=="], "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], @@ -831,7 +831,7 @@ "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "postcss-merge-rules": ["postcss-merge-rules@8.0.2", "", { "dependencies": { "browserslist": "^4.28.7", "caniuse-api": "^4.0.0", "cssnano-utils": "^6.0.2", "postcss-selector-parser": "^7.1.4" }, "peerDependencies": { "postcss": "^8.5.25" } }, "sha512-ipELN1m3k/GLGD+wmYHtiEVE2TMW5OKm4CgSRqYDeDz1ZX89AbeUCoeos0dUdTcpMilSa+zIRwvD78U3Uv/1BA=="], + "postcss-merge-rules": ["postcss-merge-rules@8.0.3", "", { "dependencies": { "browserslist": "^4.28.8", "caniuse-api": "^4.0.0", "cssnano-utils": "^6.0.3", "postcss-selector-parser": "^7.1.5" }, "peerDependencies": { "postcss": "^8.5.26" } }, "sha512-gBnrjp2ebQyrBNDqxORirC6ZM6g4cHKglAwGf1JiuKmDPjUJbNM6WhxYMYkHf+hxysnZfq27+TtxGrYESv3GMQ=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], @@ -901,7 +901,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.10.8", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.8", "@turbo/darwin-arm64": "2.10.8", "@turbo/linux-64": "2.10.8", "@turbo/linux-arm64": "2.10.8", "@turbo/windows-64": "2.10.8", "@turbo/windows-arm64": "2.10.8" }, "bin": { "turbo": "bin/turbo" } }, "sha512-9+8YX5QOkGXzZxcIykTHgaooRHGMWO+jfdyRK0o+rN0U7hBIig2MrJ8r/aNzIPDPhdA73SGb0O+tIztaModTMg=="], + "turbo": ["turbo@2.10.9", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.9", "@turbo/darwin-arm64": "2.10.9", "@turbo/linux-64": "2.10.9", "@turbo/linux-arm64": "2.10.9", "@turbo/windows-64": "2.10.9", "@turbo/windows-arm64": "2.10.9" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Yl9+ukxH+UmPtKidpDkjn82tvPoEvFNb9UACd9vUomN1Ft0cwl3rx0P8yC1D93W9EOsWRMjllvIDG8y25sFOog=="], "type-level-regexp": ["type-level-regexp@0.1.17", "", {}, "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg=="], @@ -977,10 +977,14 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "caniuse-api/browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "docs/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "json-canvas-viewer/marked": ["marked@18.0.9", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w=="], diff --git a/docs/src/pages/en/deep-dive/sync.md b/docs/src/pages/en/deep-dive/sync.md index dbb4410..c43dd54 100644 --- a/docs/src/pages/en/deep-dive/sync.md +++ b/docs/src/pages/en/deep-dive/sync.md @@ -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`. diff --git a/docs/src/pages/en/development/sync.md b/docs/src/pages/en/development/sync.md index c4e3b8b..34b5ed8 100644 --- a/docs/src/pages/en/development/sync.md +++ b/docs/src/pages/en/development/sync.md @@ -39,9 +39,9 @@ type DeciderInput = { type Decider = (input: DeciderInput) => Array; ``` -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 diff --git a/docs/src/pages/en/usage/settings.md b/docs/src/pages/en/usage/settings.md index 0549ccc..126ef3c 100644 --- a/docs/src/pages/en/usage/settings.md +++ b/docs/src/pages/en/usage/settings.md @@ -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 diff --git a/docs/src/pages/en/usage/why-sync-engine.md b/docs/src/pages/en/usage/why-sync-engine.md index 8a1925f..6530e55 100644 --- a/docs/src/pages/en/usage/why-sync-engine.md +++ b/docs/src/pages/en/usage/why-sync-engine.md @@ -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. diff --git a/manifest.json b/manifest.json index 559e46c..f74fd3c 100644 --- a/manifest.json +++ b/manifest.json @@ -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.", diff --git a/modules.json b/modules.json index 2843b18..a35580e 100644 --- a/modules.json +++ b/modules.json @@ -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", @@ -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", @@ -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", diff --git a/package.json b/package.json index 7e9faae..a107e1d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 7c37688..5208c5e 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -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" diff --git a/packages/i18n/src/ru/translations.ts b/packages/i18n/src/ru/translations.ts index b431ceb..4d6df26 100644 --- a/packages/i18n/src/ru/translations.ts +++ b/packages/i18n/src/ru/translations.ts @@ -189,6 +189,8 @@ const ru: Translations = { minRequestIntervalDescription: 'Ограничить минимальное время между последовательными запросами во время синхронизации. Полезно для сервисов с ограничением частоты запросов. Измените интервал в поле ниже.', minRequestIntervalPlaceholder: 'Введите интервал (например, 1s, 500ms)', + mirrorLocal: 'Зеркало локального хранилища', + mirrorRemote: 'Зеркало удалённого хранилища', miscellaneous: 'Разное', moduleAutoUpdate: 'Автообновление модулей', moduleAutoUpdateDescription: 'Автоматически обновлять установленные модули из их источников.', diff --git a/packages/i18n/src/zh-TW/translations.ts b/packages/i18n/src/zh-TW/translations.ts index 6f8df23..b79fcb0 100644 --- a/packages/i18n/src/zh-TW/translations.ts +++ b/packages/i18n/src/zh-TW/translations.ts @@ -182,6 +182,8 @@ const zhTW: Translations = { minRequestIntervalDescription: '限制同步過程中連續請求之間的最小時間間隔。此選項適用於有請求速率限制的服務。請在欄位中修改間隔時間。', minRequestIntervalPlaceholder: '輸入間隔時間(例如 1s, 500ms)', + mirrorLocal: '鏡像本機', + mirrorRemote: '鏡像遠端', miscellaneous: '雜項設定', moduleAutoUpdate: '自動更新模組', moduleAutoUpdateDescription: '自動從模組來源更新已安裝的模組。', diff --git a/packages/i18n/src/zh/translations.ts b/packages/i18n/src/zh/translations.ts index 9d410ea..358543e 100644 --- a/packages/i18n/src/zh/translations.ts +++ b/packages/i18n/src/zh/translations.ts @@ -169,6 +169,8 @@ const zh: Translations = { minRequestIntervalDescription: '限制同步过程中连续请求之间的最小时间间隔。此选项对于有请求频率限制的服务非常有用。在输入框中修改间隔。', minRequestIntervalPlaceholder: '输入间隔(例如 1s, 500ms)', + mirrorLocal: '镜像本地', + mirrorRemote: '镜像远程', miscellaneous: '杂项', moduleAutoUpdate: '自动更新模块', moduleAutoUpdateDescription: '从模块源自动更新已安装的模块。', diff --git a/packages/plugin/CHANGELOG.md b/packages/plugin/CHANGELOG.md index a92544d..40e4871 100644 --- a/packages/plugin/CHANGELOG.md +++ b/packages/plugin/CHANGELOG.md @@ -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. diff --git a/packages/plugin/dist/dev.spec.d.ts b/packages/plugin/dist/dev.spec.d.ts index 36a2be7..cbfe468 100644 --- a/packages/plugin/dist/dev.spec.d.ts +++ b/packages/plugin/dist/dev.spec.d.ts @@ -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 @@ -28,6 +28,12 @@ type RequestHarness = { calls: Array; 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; @@ -35,6 +41,16 @@ declare function file(key: string, options?: { 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; +declare function taskNames(tasks: Array): Array; +declare function taskKeys(tasks: Array): Array; +declare function findTask(tasks: Array, key: string): ExtractedTask; declare function stream(chunks?: Array): ReadableStream; declare function deferred(): { promise: Promise; @@ -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 diff --git a/packages/plugin/dist/index-Ddt5UAXp.spec.d.ts b/packages/plugin/dist/index-BJDjAUwk.spec.d.ts similarity index 99% rename from packages/plugin/dist/index-Ddt5UAXp.spec.d.ts rename to packages/plugin/dist/index-BJDjAUwk.spec.d.ts index 5d1fbe0..d7a34f2 100644 --- a/packages/plugin/dist/index-Ddt5UAXp.spec.d.ts +++ b/packages/plugin/dist/index-BJDjAUwk.spec.d.ts @@ -908,6 +908,8 @@ declare class Bootstrap { private remoteFs?; readonly i18n: { bidirectional: string; + mirrorLocal: string; + mirrorRemote: string; latestSurvive: string; keepLocal: string; keepRemote: string; diff --git a/packages/plugin/dist/index.spec.d.ts b/packages/plugin/dist/index.spec.d.ts index 0a43aeb..0a4e937 100644 --- a/packages/plugin/dist/index.spec.d.ts +++ b/packages/plugin/dist/index.spec.d.ts @@ -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 }; \ No newline at end of file diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 62ea660..16e51d8 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -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", @@ -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", diff --git a/packages/plugin/src/en.ts b/packages/plugin/src/en.ts index 0ac9979..f5d36f3 100644 --- a/packages/plugin/src/en.ts +++ b/packages/plugin/src/en.ts @@ -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.', diff --git a/packages/plugin/src/fs/wrappers/asymmetric-storage.ts b/packages/plugin/src/fs/wrappers/asymmetric-storage.ts index c526e33..1eec547 100644 --- a/packages/plugin/src/fs/wrappers/asymmetric-storage.ts +++ b/packages/plugin/src/fs/wrappers/asymmetric-storage.ts @@ -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); diff --git a/packages/plugin/src/modules/Bootstrap.ts b/packages/plugin/src/modules/Bootstrap.ts index 3646ec1..f610d29 100644 --- a/packages/plugin/src/modules/Bootstrap.ts +++ b/packages/plugin/src/modules/Bootstrap.ts @@ -36,6 +36,8 @@ import headSettings from '@/settings/head'; import miscellaneousSettings from '@/settings/miscellaneous'; import { bidirectionalDecider, + mirrorLocalDecider, + mirrorRemoteDecider, keepLocalResolver, keepRemoteResolver, latestSurviveResolver, @@ -85,6 +87,8 @@ export default class Bootstrap { declare readonly i18n: { bidirectional: string; + mirrorLocal: string; + mirrorRemote: string; latestSurvive: string; keepLocal: string; keepRemote: string; @@ -360,6 +364,14 @@ export default class Bootstrap { decider: bidirectionalDecider, prettyName: () => t('bidirectional'), }); + registerDecider('mirrorLocal', { + decider: mirrorLocalDecider, + prettyName: () => t('mirrorLocal'), + }); + registerDecider('mirrorRemote', { + decider: mirrorRemoteDecider, + prettyName: () => t('mirrorRemote'), + }); registerConflictResolver('renameAndKeepBoth', { prettyName: () => t('renameAndKeepBoth'), diff --git a/packages/plugin/src/sync/decision/mirror.ts b/packages/plugin/src/sync/decision/mirror.ts new file mode 100644 index 0000000..a581cfb --- /dev/null +++ b/packages/plugin/src/sync/decision/mirror.ts @@ -0,0 +1,103 @@ +import type { FileStat } from '@/types'; +import type { BaseTask } from '../tasks/interface'; +import type { DeciderInput } from './interface'; + +type Source = 'local' | 'remote'; + +export function mirrorLocalDecider(input: DeciderInput): Array { + return mirrorDecider(input, 'local'); +} + +export function mirrorRemoteDecider(input: DeciderInput): Array { + return mirrorDecider(input, 'remote'); +} + +function mirrorDecider(input: DeciderInput, source: Source): Array { + const { localStats, logger, records, remoteStats, taskFactory } = input; + const sourceStats = source === 'local' ? localStats : remoteStats; + const targetStats = source === 'local' ? remoteStats : localStats; + const tasks: Array = []; + + for (const key of new Set([...sourceStats.keys(), ...targetStats.keys(), ...records.keys()])) { + const sourceStat = sourceStats.get(key); + const targetStat = targetStats.get(key); + const record = records.get(key); + + if (!sourceStat) { + if (targetStat) { + logger( + `Decider: remove ${source === 'local' ? 'remote' : 'local'} \`${key}\`, reason: mirror ${source}.`, + ); + tasks.push( + source === 'local' + ? taskFactory('removeRemote', { key, remote: targetStat }) + : taskFactory('removeLocal', { key, local: targetStat }), + ); + } else if (record) tasks.push(taskFactory('removeRecord', { key })); + continue; + } + + const create = () => { + logger(`Decider: mirror ${source} \`${key}\`, reason: target missing or incompatible.`); + if (sourceStat.isDir) + tasks.push( + source === 'local' + ? taskFactory('createRemoteDir', { key, local: sourceStat }) + : taskFactory('createLocalDir', { key, remote: sourceStat }), + ); + else + tasks.push( + source === 'local' + ? taskFactory('upload', { key, local: sourceStat }) + : taskFactory('download', { key, remote: sourceStat }), + ); + }; + + if (!targetStat) { + create(); + continue; + } + if (sourceStat.isDir !== targetStat.isDir) { + tasks.push( + source === 'local' + ? taskFactory('removeRemote', { key, remote: targetStat }) + : taskFactory('removeLocal', { key, local: targetStat }), + ); + create(); + continue; + } + if (sourceStat.isDir) { + if (!record) + tasks.push( + taskFactory('addRecord', { + key, + ...(source === 'local' + ? { local: sourceStat, remote: targetStat } + : { local: targetStat, remote: sourceStat }), + }), + ); + continue; + } + const targetFile = targetStat as FileStat; + if (!record && sourceStat.size === targetFile.size) { + tasks.push( + taskFactory('addRecord', { + key, + ...(source === 'local' + ? { local: sourceStat, remote: targetFile } + : { local: targetFile, remote: sourceStat }), + }), + ); + continue; + } + if ( + !record || + record.isDir || + record[source] !== sourceStat.uid || + record[source === 'local' ? 'remote' : 'local'] !== targetFile.uid + ) + create(); + } + + return tasks; +} diff --git a/packages/plugin/src/sync/index.ts b/packages/plugin/src/sync/index.ts index dfe72f0..a77d277 100644 --- a/packages/plugin/src/sync/index.ts +++ b/packages/plugin/src/sync/index.ts @@ -27,6 +27,7 @@ export { taskMap } from './decision/interface'; export { getTaskColor, getTaskIcon } from './tasks/interface'; export { default as detectMoves } from './utils/detect-moves'; export { default as bidirectionalDecider } from './decision/bidirectional'; +export { mirrorLocalDecider, mirrorRemoteDecider } from './decision/mirror'; export { default as keepRemoteResolver } from './conflict-resolve/keep-remote'; export { default as keepLocalResolver } from './conflict-resolve/keep-local'; export { default as latestSurviveResolver } from './conflict-resolve/latest-survive'; diff --git a/packages/plugin/test/bidirectional.test.ts b/packages/plugin/test/bidirectional.test.ts index 8a205af..9b7613b 100644 --- a/packages/plugin/test/bidirectional.test.ts +++ b/packages/plugin/test/bidirectional.test.ts @@ -1,103 +1,38 @@ import testKit from '$/test-kit'; import { expect, test } from 'bun:test'; -import type { TaskFactory, TaskNames, TaskOptions } from '@/sync'; -import type { FileStat, RecordStat, RecordStatsMap, Stat, StatsMap } from '@/types'; -import { bidirectionalDecider, taskMap } from '@/sync'; - -const { file: kitFile, folder } = testKit; - -function file(key: string, uid = `${key}-uid`): FileStat { - return kitFile(key, { uid }); -} - -function fileRecord(local: string, remote: string): RecordStat { - return { isDir: false, local, remote }; -} - -function folderRecord(): RecordStat { - return { isDir: true }; -} - -const NOOP = () => {}; -const VOID = {} as never; - -type ExtractedTask = { - name: TaskNames; - key: string; - local?: Stat; - remote?: Stat; -}; - -function runDecider(input: { - localStats?: StatsMap; - remoteStats?: StatsMap; - records?: RecordStatsMap; -}): Array { - const localStats = input.localStats ?? new Map(); - const remoteStats = input.remoteStats ?? new Map(); - const records = input.records ?? new Map(); - const captured: Array = []; - - const taskFactory = ((name: TaskNames, options: TaskOptions) => { - const TaskCtor = taskMap[name]; - const task = new TaskCtor({ - ...options, - localFs: VOID, - record: VOID, - remoteFs: VOID, - } as never); - task.name = name; - task.prettyName = name; - captured.push({ - key: options.key, - name, - ...(options.local !== undefined && { local: options.local }), - ...(options.remote !== undefined && { remote: options.remote }), - }); - return task; - }) as TaskFactory; - - bidirectionalDecider({ localStats, logger: NOOP, records, remoteStats, taskFactory }); - - return captured; -} - -function names(tasks: Array): Array { - return tasks.map((t) => t.name); -} - -function keys(tasks: Array): Array { - return tasks.map((t) => t.key); -} - -/** Asserts exactly one task in the group has the given key, and returns it. */ -function keyed(tasks: Array, key: string): ExtractedTask { - const matching = tasks.filter((t) => t.key === key); - expect(matching).toHaveLength(1); - return matching[0]; -} +import type { RecordStatsMap, Stat, StatsMap } from '@/types'; +import { bidirectionalDecider } from '@/sync'; + +const { file, fileRecord, findTask, folder, folderRecord, runDecider, taskKeys, taskNames } = + testKit; test('file only local, no record → upload', () => { - const local = file('a.md', 'a-uid'); - const task = keyed(runDecider({ localStats: new Map([['a.md', local]]) }), 'a.md'); + const local = file('a.md', { uid: 'a-uid' }); + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]) }), + 'a.md', + ); expect(task.name).toBe('upload'); expect(task.local).toBe(local); }); test('file only remote, no record → download', () => { - const remote = file('a.md', 'a-uid'); - const task = keyed(runDecider({ remoteStats: new Map([['a.md', remote]]) }), 'a.md'); + const remote = file('a.md', { uid: 'a-uid' }); + const task = findTask( + runDecider(bidirectionalDecider, { remoteStats: new Map([['a.md', remote]]) }), + 'a.md', + ); expect(task.name).toBe('download'); expect(task.remote).toBe(remote); }); test('file both sides, no record, same size → addRecord', () => { - const local = file('a.md', 'local-uid'); - const remote = file('a.md', 'remote-uid'); - const task = keyed( - runDecider({ + const local = file('a.md', { uid: 'local-uid' }); + const remote = file('a.md', { uid: 'remote-uid' }); + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), remoteStats: new Map([['a.md', remote]]), }), @@ -110,10 +45,10 @@ test('file both sides, no record, same size → addRecord', () => { }); test('file both sides, no record, different size → resolveConflict', () => { - const local = file('a.md', 'local-uid'); - const remote = { ...file('a.md', 'remote-uid'), size: 999 }; - const task = keyed( - runDecider({ + const local = file('a.md', { uid: 'local-uid' }); + const remote = { ...file('a.md', { uid: 'remote-uid' }), size: 999 }; + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), remoteStats: new Map([['a.md', remote]]), }), @@ -124,12 +59,12 @@ test('file both sides, no record, different size → resolveConflict', () => { }); test('file with record, both unchanged → no tasks', () => { - const local = file('a.md', 'local-uid'); - const remote = file('a.md', 'remote-uid'); + const local = file('a.md', { uid: 'local-uid' }); + const remote = file('a.md', { uid: 'remote-uid' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('local-uid', 'remote-uid')]]); expect( - runDecider({ + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records, remoteStats: new Map([['a.md', remote]]), @@ -138,11 +73,11 @@ test('file with record, both unchanged → no tasks', () => { }); test('file with record, both changed → resolveConflict', () => { - const local = file('a.md', 'new-local'); - const remote = file('a.md', 'new-remote'); + const local = file('a.md', { uid: 'new-local' }); + const remote = file('a.md', { uid: 'new-remote' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('old-local', 'old-remote')]]); - const task = keyed( - runDecider({ + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records, remoteStats: new Map([['a.md', remote]]), @@ -156,11 +91,11 @@ test('file with record, both changed → resolveConflict', () => { }); test('file with record, only remote changed → download', () => { - const local = file('a.md', 'local-uid'); - const remote = file('a.md', 'new-remote'); + const local = file('a.md', { uid: 'local-uid' }); + const remote = file('a.md', { uid: 'new-remote' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('local-uid', 'old-remote')]]); - const task = keyed( - runDecider({ + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records, remoteStats: new Map([['a.md', remote]]), @@ -173,11 +108,11 @@ test('file with record, only remote changed → download', () => { }); test('file with record, only local changed → upload', () => { - const local = file('a.md', 'new-local'); - const remote = file('a.md', 'remote-uid'); + const local = file('a.md', { uid: 'new-local' }); + const remote = file('a.md', { uid: 'remote-uid' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('old-local', 'remote-uid')]]); - const task = keyed( - runDecider({ + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records, remoteStats: new Map([['a.md', remote]]), @@ -190,36 +125,48 @@ test('file with record, only local changed → upload', () => { }); test('file with record, no remote, local changed → upload', () => { - const local = file('a.md', 'new-local'); + const local = file('a.md', { uid: 'new-local' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('old-local', 'old-remote')]]); - const task = keyed(runDecider({ localStats: new Map([['a.md', local]]), records }), 'a.md'); + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records }), + 'a.md', + ); expect(task.name).toBe('upload'); expect(task.local).toBe(local); }); test('file with record, no remote, local unchanged → removeLocal', () => { - const local = file('a.md', 'local-uid'); + const local = file('a.md', { uid: 'local-uid' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('local-uid', 'old-remote')]]); - const task = keyed(runDecider({ localStats: new Map([['a.md', local]]), records }), 'a.md'); + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['a.md', local]]), records }), + 'a.md', + ); expect(task.name).toBe('removeLocal'); expect(task.local).toBe(local); }); test('file with record, no local, remote changed → download', () => { - const remote = file('a.md', 'new-remote'); + const remote = file('a.md', { uid: 'new-remote' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('old-local', 'old-remote')]]); - const task = keyed(runDecider({ records, remoteStats: new Map([['a.md', remote]]) }), 'a.md'); + const task = findTask( + runDecider(bidirectionalDecider, { records, remoteStats: new Map([['a.md', remote]]) }), + 'a.md', + ); expect(task.name).toBe('download'); expect(task.remote).toBe(remote); }); test('file with record, no local, remote unchanged → removeRemote', () => { - const remote = file('a.md', 'remote-uid'); + const remote = file('a.md', { uid: 'remote-uid' }); const records: RecordStatsMap = new Map([['a.md', fileRecord('old-local', 'remote-uid')]]); - const task = keyed(runDecider({ records, remoteStats: new Map([['a.md', remote]]) }), 'a.md'); + const task = findTask( + runDecider(bidirectionalDecider, { records, remoteStats: new Map([['a.md', remote]]) }), + 'a.md', + ); expect(task.name).toBe('removeRemote'); expect(task.remote).toBe(remote); @@ -227,7 +174,10 @@ test('file with record, no local, remote unchanged → removeRemote', () => { test('folder only local, no record → createRemoteDir', () => { const local = folder('docs/'); - const task = keyed(runDecider({ localStats: new Map([['docs/', local]]) }), 'docs/'); + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['docs/', local]]) }), + 'docs/', + ); expect(task.name).toBe('createRemoteDir'); expect(task.local).toBe(local); @@ -235,7 +185,10 @@ test('folder only local, no record → createRemoteDir', () => { test('folder only remote, no record → createLocalDir', () => { const remote = folder('docs/'); - const task = keyed(runDecider({ remoteStats: new Map([['docs/', remote]]) }), 'docs/'); + const task = findTask( + runDecider(bidirectionalDecider, { remoteStats: new Map([['docs/', remote]]) }), + 'docs/', + ); expect(task.name).toBe('createLocalDir'); expect(task.remote).toBe(remote); @@ -243,8 +196,8 @@ test('folder only remote, no record → createLocalDir', () => { test('folder both sides, no record → addRecord', () => { const dir = folder('docs/'); - const task = keyed( - runDecider({ + const task = findTask( + runDecider(bidirectionalDecider, { localStats: new Map([['docs/', dir]]), remoteStats: new Map([['docs/', dir]]), }), @@ -258,7 +211,7 @@ test('folder with record, both sides unchanged → no tasks', () => { const dir = folder('docs/'); const rec = folderRecord(); expect( - runDecider({ + runDecider(bidirectionalDecider, { localStats: new Map([['docs/', dir]]), records: new Map([['docs/', rec]]), remoteStats: new Map([['docs/', dir]]), @@ -269,22 +222,22 @@ test('folder with record, both sides unchanged → no tasks', () => { test('folder with record, no remote, content changed → createRemoteDir', () => { // A new subfile has no record entry → folder detected as changed. // The subfile itself also generates an upload task. - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { localStats: new Map([ ['docs/', folder('docs/')], - ['docs/note.md', file('docs/note.md', 'note-uid')], + ['docs/note.md', file('docs/note.md', { uid: 'note-uid' })], ]), records: new Map([['docs/', folderRecord()]]), }); - expect(names(tasks)).toStrictEqual(['upload', 'createRemoteDir']); + expect(taskNames(tasks)).toStrictEqual(['upload', 'createRemoteDir']); expect(tasks[1].key).toBe('docs/'); }); test('folder with record, no remote, content unchanged → removeLocal', () => { // All subfolders have records → isChanged returns false → removeLocal. // Both docs/ and docs/sub/ are unchanged folders with no remote. - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { localStats: new Map([ ['docs/', folder('docs/')], ['docs/sub/', folder('docs/sub/')], @@ -295,25 +248,25 @@ test('folder with record, no remote, content unchanged → removeLocal', () => { ]), }); - expect(names(tasks)).toStrictEqual(['removeLocal', 'removeLocal']); + expect(taskNames(tasks)).toStrictEqual(['removeLocal', 'removeLocal']); expect(tasks[0].key).toBe('docs/'); }); test('folder with record, no local, remote content changed → createLocalDir', () => { - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { records: new Map([['docs/', folderRecord()]]), remoteStats: new Map([ ['docs/', folder('docs/')], - ['docs/note.md', file('docs/note.md', 'note-uid')], + ['docs/note.md', file('docs/note.md', { uid: 'note-uid' })], ]), }); - expect(names(tasks)).toStrictEqual(['download', 'createLocalDir']); + expect(taskNames(tasks)).toStrictEqual(['download', 'createLocalDir']); expect(tasks[1].key).toBe('docs/'); }); test('folder with record, no local, remote content unchanged → removeRemote', () => { - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { records: new Map([ ['docs/', folderRecord()], ['docs/sub/', folderRecord()], @@ -324,95 +277,100 @@ test('folder with record, no local, remote content unchanged → removeRemote', ]), }); - expect(names(tasks)).toStrictEqual(['removeRemote', 'removeRemote']); + expect(taskNames(tasks)).toStrictEqual(['removeRemote', 'removeRemote']); expect(tasks[0].key).toBe('docs/'); }); test('file-folder mismatch, both changed → throws', () => { // Record is fileRecord; local=folder, remote=file → both changed (type mismatch) expect(() => - runDecider({ + runDecider(bidirectionalDecider, { localStats: new Map([['item', folder('item')]]), records: new Map([['item', fileRecord('local-uid', 'old-remote')]]), - remoteStats: new Map([['item', file('item', 'remote-uid')]]), + remoteStats: new Map([['item', file('item', { uid: 'remote-uid' })]]), }), ).toThrow('Unable to sync: item is a file at remote but a folder at local'); }); test('file-folder mismatch, no record → throws', () => { expect(() => - runDecider({ + runDecider(bidirectionalDecider, { localStats: new Map([['item', folder('item')]]), - remoteStats: new Map([['item', file('item', 'remote-uid')]]), + remoteStats: new Map([['item', file('item', { uid: 'remote-uid' })]]), }), ).toThrow('Unable to sync: item is a file at remote but a folder at local'); }); test('file-folder: local became dir, remote file unchanged → removeRemote + createRemoteDir', () => { // Record was fileRecord matching remote uid → remote unchanged, local changed - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { localStats: new Map([['item', folder('item')]]), records: new Map([['item', fileRecord('local-uid', 'remote-uid')]]), - remoteStats: new Map([['item', file('item', 'remote-uid')]]), + remoteStats: new Map([['item', file('item', { uid: 'remote-uid' })]]), }); - expect(names(tasks)).toStrictEqual(['removeRemote', 'createRemoteDir']); + expect(taskNames(tasks)).toStrictEqual(['removeRemote', 'createRemoteDir']); }); test('file-folder: local became file, remote folder unchanged → removeRemote + upload', () => { // Record was folderRecord → remote folder unchanged, local changed - const tasks = runDecider({ - localStats: new Map([['item', file('item', 'new-local')]]), + const tasks = runDecider(bidirectionalDecider, { + localStats: new Map([['item', file('item', { uid: 'new-local' })]]), records: new Map([['item', folderRecord()]]), remoteStats: new Map([['item', folder('item')]]), }); - expect(names(tasks)).toStrictEqual(['removeRemote', 'upload']); + expect(taskNames(tasks)).toStrictEqual(['removeRemote', 'upload']); }); test('file-folder: remote became dir, local file unchanged → removeLocal + createLocalDir', () => { // Record was fileRecord matching local uid → local unchanged, remote changed - const tasks = runDecider({ - localStats: new Map([['item', file('item', 'local-uid')]]), + const tasks = runDecider(bidirectionalDecider, { + localStats: new Map([['item', file('item', { uid: 'local-uid' })]]), records: new Map([['item', fileRecord('local-uid', 'old-remote')]]), remoteStats: new Map([['item', folder('item')]]), }); - expect(names(tasks)).toStrictEqual(['removeLocal', 'createLocalDir']); + expect(taskNames(tasks)).toStrictEqual(['removeLocal', 'createLocalDir']); }); test('file-folder: remote became file, local folder unchanged → removeLocal + download', () => { // Record was folderRecord → local folder unchanged, remote changed - const tasks = runDecider({ + const tasks = runDecider(bidirectionalDecider, { localStats: new Map([['item', folder('item')]]), records: new Map([['item', folderRecord()]]), - remoteStats: new Map([['item', file('item', 'new-remote')]]), + remoteStats: new Map([['item', file('item', { uid: 'new-remote' })]]), }); - expect(names(tasks)).toStrictEqual(['removeLocal', 'download']); + expect(taskNames(tasks)).toStrictEqual(['removeLocal', 'download']); }); test('key only in records (deleted from both sides) → removeRecord', () => { const records: RecordStatsMap = new Map([['gone.md', fileRecord('local-uid', 'remote-uid')]]); - const task = keyed(runDecider({ records }), 'gone.md'); + const task = findTask(runDecider(bidirectionalDecider, { records }), 'gone.md'); expect(task.name).toBe('removeRecord'); }); test('multiple items produce tasks in file→folder→cleanup order', () => { const localStats: StatsMap = new Map([ - ['notes/new.md', file('notes/new.md', 'new-uid')], + ['notes/new.md', file('notes/new.md', { uid: 'new-uid' })], ['photos/', folder('photos/')], ]); - const remoteStats: StatsMap = new Map([['stale.md', file('stale.md', 'stale-uid')]]); + const remoteStats: StatsMap = new Map([['stale.md', file('stale.md', { uid: 'stale-uid' })]]); const records: RecordStatsMap = new Map([['deleted.md', fileRecord('d-local', 'd-remote')]]); - const tasks = runDecider({ localStats, records, remoteStats }); + const tasks = runDecider(bidirectionalDecider, { localStats, records, remoteStats }); - expect(names(tasks)).toStrictEqual(['upload', 'download', 'createRemoteDir', 'removeRecord']); - expect(keys(tasks)).toStrictEqual(['notes/new.md', 'stale.md', 'photos/', 'deleted.md']); + expect(taskNames(tasks)).toStrictEqual([ + 'upload', + 'download', + 'createRemoteDir', + 'removeRecord', + ]); + expect(taskKeys(tasks)).toStrictEqual(['notes/new.md', 'stale.md', 'photos/', 'deleted.md']); }); test('empty inputs produce no tasks', () => { - expect(runDecider({})).toHaveLength(0); + expect(runDecider(bidirectionalDecider, {})).toHaveLength(0); }); diff --git a/packages/plugin/test/mirror.test.ts b/packages/plugin/test/mirror.test.ts new file mode 100644 index 0000000..ac490a3 --- /dev/null +++ b/packages/plugin/test/mirror.test.ts @@ -0,0 +1,103 @@ +import testKit from '$/test-kit'; +import { expect, test } from 'bun:test'; +import type { Decider } from '@/sync'; +import { mirrorLocalDecider, mirrorRemoteDecider } from '@/sync'; + +const { file, fileRecord, findTask, folder, folderRecord, runDecider, taskNames } = testKit; + +const strategies: Array<[string, Decider, 'local' | 'remote']> = [ + ['mirror local', mirrorLocalDecider, 'local'], + ['mirror remote', mirrorRemoteDecider, 'remote'], +]; + +for (const [name, decider, source] of strategies) { + const target = source === 'local' ? 'remote' : 'local'; + const sourceStats = (stat: ReturnType | ReturnType) => + source === 'local' + ? { localStats: new Map([[stat.key, stat]]) } + : { remoteStats: new Map([[stat.key, stat]]) }; + const targetStats = (stat: ReturnType | ReturnType) => + source === 'local' + ? { remoteStats: new Map([[stat.key, stat]]) } + : { localStats: new Map([[stat.key, stat]]) }; + + test(`${name} copies an authoritative file`, () => { + const stat = file('note.md', { uid: 'source' }); + const task = findTask(runDecider(decider, sourceStats(stat)), 'note.md'); + + expect(task.name).toBe(source === 'local' ? 'upload' : 'download'); + expect(task[source]).toBe(stat); + }); + + test(`${name} removes a target-only file`, () => { + const stat = file('note.md', { uid: 'target' }); + const task = findTask(runDecider(decider, targetStats(stat)), 'note.md'); + + expect(task.name).toBe(source === 'local' ? 'removeRemote' : 'removeLocal'); + expect(task[target]).toBe(stat); + }); + + test(`${name} records a same-size unrecorded file`, () => { + const sourceFile = file('note.md', { uid: 'source' }); + const targetFile = file('note.md', { uid: 'target' }); + const task = findTask( + runDecider(decider, { ...sourceStats(sourceFile), ...targetStats(targetFile) }), + 'note.md', + ); + + expect(task.name).toBe('addRecord'); + }); + + test(`${name} overwrites a differently sized unrecorded file`, () => { + const sourceFile = file('note.md', { uid: 'source' }); + const targetFile = file('note.md', { size: 99, uid: 'target' }); + const task = findTask( + runDecider(decider, { ...sourceStats(sourceFile), ...targetStats(targetFile) }), + 'note.md', + ); + + expect(task.name).toBe(source === 'local' ? 'upload' : 'download'); + }); + + test(`${name} skips a recorded file that matches both sides`, () => { + const local = file('note.md', { uid: 'local' }); + const remote = file('note.md', { uid: 'remote' }); + + expect( + runDecider(decider, { + localStats: new Map([['note.md', local]]), + records: new Map([['note.md', fileRecord('local', 'remote')]]), + remoteStats: new Map([['note.md', remote]]), + }), + ).toHaveLength(0); + }); + + test(`${name} replaces a target folder with an authoritative file`, () => { + const sourceFile = file('entry', { uid: 'source' }); + const tasks = runDecider(decider, { + ...sourceStats(sourceFile), + ...targetStats(folder('entry')), + }); + + expect(taskNames(tasks)).toStrictEqual([ + source === 'local' ? 'removeRemote' : 'removeLocal', + source === 'local' ? 'upload' : 'download', + ]); + }); + + test(`${name} creates an authoritative folder and clears stale records`, () => { + const sourceFolder = folder('docs/'); + const tasks = runDecider(decider, { + ...sourceStats(sourceFolder), + records: new Map([ + ['docs/', folderRecord()], + ['gone.md', fileRecord('old-local', 'old-remote')], + ]), + }); + + expect(taskNames(tasks)).toStrictEqual([ + source === 'local' ? 'createRemoteDir' : 'createLocalDir', + 'removeRecord', + ]); + }); +} diff --git a/packages/plugin/test/test-kit.ts b/packages/plugin/test/test-kit.ts index a681575..63022be 100644 --- a/packages/plugin/test/test-kit.ts +++ b/packages/plugin/test/test-kit.ts @@ -1,6 +1,16 @@ import type { Fs, ListReporter, RootFs } from '@/fs'; import type { Request, RequestParam } from '@/modules/Registrar'; -import type { Stat, Binary, FileStat, FolderStat } from '@/types'; +import type { Decider, TaskFactory, TaskNames, TaskOptions } from '@/sync'; +import type { + Binary, + FileStat, + FolderStat, + RecordStat, + RecordStatsMap, + Stat, + StatsMap, +} from '@/types'; +import { taskMap } from '@/sync'; type FsCalls = { delete: Array; @@ -31,6 +41,13 @@ type RequestHarness = { request: Request; }; +type ExtractedTask = { + key: string; + local?: Stat; + name: TaskNames; + remote?: Stat; +}; + const textEncoder = new TextEncoder(); function bytes(value: string): Binary { @@ -49,6 +66,60 @@ function folder(key: string): FolderStat { return { isDir: true, key }; } +function fileRecord(local: string, remote: string): RecordStat { + return { isDir: false, local, remote }; +} + +function folderRecord(): RecordStat { + return { isDir: true }; +} + +function runDecider( + decider: Decider, + input: { localStats?: StatsMap; remoteStats?: StatsMap; records?: RecordStatsMap }, +): Array { + const localStats = input.localStats ?? new Map(); + const remoteStats = input.remoteStats ?? new Map(); + const records = input.records ?? new Map(); + const tasks: Array = []; + const taskFactory = ((name: TaskNames, options: TaskOptions) => { + const Task = taskMap[name]; + const task = new Task({ + ...options, + localFs: {} as never, + record: {} as never, + remoteFs: {} as never, + } as never); + task.name = name; + task.prettyName = name; + tasks.push({ + key: options.key, + name, + ...(options.local !== undefined && { local: options.local }), + ...(options.remote !== undefined && { remote: options.remote }), + }); + return task; + }) as TaskFactory; + + decider({ localStats, logger: () => {}, records, remoteStats, taskFactory }); + return tasks; +} + +function taskNames(tasks: Array): Array { + return tasks.map((task) => task.name); +} + +function taskKeys(tasks: Array): Array { + return tasks.map((task) => task.key); +} + +function findTask(tasks: Array, key: string): ExtractedTask { + const matching = tasks.filter((task) => task.key === key); + if (matching.length !== 1) + throw new Error(`Expected one task for ${key}, found ${matching.length}.`); + return matching[0]; +} + function defaultStat(key: string): Stat { return key === '/' || key.endsWith('/') ? folder(key) @@ -181,6 +252,21 @@ function fs(options: FsOptions = {}): FsHarness { return { calls, control, fs: rootFs }; } -const testKit = { bytes, deferred, file, flush, folder, fs, request, stream }; +const testKit = { + bytes, + deferred, + file, + fileRecord, + findTask, + flush, + folder, + folderRecord, + fs, + request, + runDecider, + stream, + taskKeys, + taskNames, +}; export default testKit; diff --git a/packages/smart-merge/src/utils/merge.ts b/packages/smart-merge/src/utils/merge.ts index 61dec58..17ee298 100644 --- a/packages/smart-merge/src/utils/merge.ts +++ b/packages/smart-merge/src/utils/merge.ts @@ -102,9 +102,9 @@ function mergeRecursive(params: MergeSegments, options: MergeOptions): string { const originalOIndex = o.index; const originalBIndex = b.index; mergeResult.merged.forEach((token, index) => { - let aJoint: string | undefined, - bJoint: string | undefined, - oJoint: string | undefined; + let aJoint: string | undefined; + let bJoint: string | undefined; + let oJoint: string | undefined; if (chunkA[index]) { aJoint = a.joints[a.index]; a.index++; diff --git a/versions.json b/versions.json index 43b16b3..a5bf88d 100644 --- a/versions.json +++ b/versions.json @@ -4,5 +4,6 @@ "3.0.2": "1.12.3", "3.0.3": "1.12.3", "3.0.4": "1.12.3", - "3.0.5": "1.12.3" + "3.0.5": "1.12.3", + "3.0.6": "1.12.3" }