Add API for propagating cursor locations (#61)
This commit is contained in:
parent
f97193e287
commit
e8b9bf40c5
80 changed files with 1930 additions and 2229 deletions
35
.gitignore
vendored
35
.gitignore
vendored
|
|
@ -1,17 +1,18 @@
|
||||||
# npm
|
# npm
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Exclude macOS Finder (System Explorer) View States
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Rust build folder
|
# Rust build folder
|
||||||
backend/target
|
backend/target
|
||||||
|
|
||||||
frontend/*/dist
|
# Frontend build folders
|
||||||
|
frontend/*/dist
|
||||||
backend/db.sqlite3*
|
|
||||||
backend/databases
|
backend/db.sqlite3*
|
||||||
|
backend/databases
|
||||||
*.log
|
backend/sync_server/bindings/*.ts
|
||||||
|
|
||||||
*.sqlx
|
*.log
|
||||||
|
*.sqlx
|
||||||
|
|
|
||||||
381
backend/Cargo.lock
generated
381
backend/Cargo.lock
generated
|
|
@ -17,20 +17,6 @@ version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"getrandom 0.2.15",
|
|
||||||
"once_cell",
|
|
||||||
"serde",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy 0.7.35",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -40,41 +26,6 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aide"
|
|
||||||
version = "0.13.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5678d2978845ddb4bd736a026f467dd652d831e9e6254b0e41b07f7ee7523309"
|
|
||||||
dependencies = [
|
|
||||||
"axum",
|
|
||||||
"axum-extra",
|
|
||||||
"bytes",
|
|
||||||
"cfg-if",
|
|
||||||
"http",
|
|
||||||
"indexmap",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_qs",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aide-axum-typed-multipart"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b8f5c830a08754addfa31fa09e6c183bac8d2ae7bd007131f9eb84fcb87a40e"
|
|
||||||
dependencies = [
|
|
||||||
"aide",
|
|
||||||
"axum",
|
|
||||||
"axum_typed_multipart",
|
|
||||||
"indexmap",
|
|
||||||
"schemars",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
|
|
@ -265,26 +216,6 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-jsonschema"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fcffe29ca1b60172349fea781ec34441d598809bd227ccbb5bf5dc2879cd9c78"
|
|
||||||
dependencies = [
|
|
||||||
"aide",
|
|
||||||
"async-trait",
|
|
||||||
"axum",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"itertools",
|
|
||||||
"jsonschema",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_path_to_error",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
|
@ -368,21 +299,6 @@ version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bit-set"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
|
||||||
dependencies = [
|
|
||||||
"bit-vec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bit-vec"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
|
@ -407,12 +323,6 @@ version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecount"
|
|
||||||
version = "0.6.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -662,6 +572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -699,12 +610,6 @@ version = "0.15.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dyn-clone"
|
|
||||||
version = "1.0.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
|
|
@ -767,16 +672,6 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fancy-regex"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
|
||||||
dependencies = [
|
|
||||||
"bit-set",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -815,16 +710,6 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fraction"
|
|
||||||
version = "0.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"num",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|
@ -942,10 +827,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -966,6 +849,12 @@ version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
|
|
@ -983,7 +872,7 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1298,6 +1187,17 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
|
|
@ -1305,7 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1328,24 +1228,6 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iso8601"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
|
@ -1362,34 +1244,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jsonschema"
|
|
||||||
version = "0.17.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978"
|
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
"anyhow",
|
|
||||||
"base64 0.21.7",
|
|
||||||
"bytecount",
|
|
||||||
"fancy-regex",
|
|
||||||
"fraction",
|
|
||||||
"getrandom 0.2.15",
|
|
||||||
"iso8601",
|
|
||||||
"itoa",
|
|
||||||
"memchr",
|
|
||||||
"num-cmp",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"percent-encoding",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"time",
|
|
||||||
"url",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -1503,12 +1357,6 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -1546,16 +1394,6 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
|
@ -1566,30 +1404,6 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-bigint"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
|
@ -1607,21 +1421,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-cmp"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-complex"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1648,17 +1447,6 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-rational"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -2059,34 +1847,6 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "schemars"
|
|
||||||
version = "0.8.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"chrono",
|
|
||||||
"dyn-clone",
|
|
||||||
"indexmap",
|
|
||||||
"schemars_derive",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "schemars_derive"
|
|
||||||
version = "0.8.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"serde_derive_internals",
|
|
||||||
"syn 2.0.90",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -2119,17 +1879,6 @@ dependencies = [
|
||||||
"syn 2.0.90",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive_internals"
|
|
||||||
version = "0.29.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.90",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.140"
|
version = "1.0.140"
|
||||||
|
|
@ -2152,19 +1901,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_qs"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
|
|
||||||
dependencies = [
|
|
||||||
"axum",
|
|
||||||
"futures",
|
|
||||||
"percent-encoding",
|
|
||||||
"serde",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
@ -2177,13 +1913,43 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.7.0",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.90",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.9.34+deprecated"
|
version = "0.9.34+deprecated"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.7.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2329,9 +2095,9 @@ dependencies = [
|
||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"indexmap",
|
"indexmap 2.7.0",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2562,12 +2328,9 @@ dependencies = [
|
||||||
name = "sync_server"
|
name = "sync_server"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aide",
|
|
||||||
"aide-axum-typed-multipart",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-jsonschema",
|
|
||||||
"axum_typed_multipart",
|
"axum_typed_multipart",
|
||||||
"bimap",
|
"bimap",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -2578,9 +2341,9 @@ dependencies = [
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"regex",
|
"regex",
|
||||||
"sanitize-filename",
|
"sanitize-filename",
|
||||||
"schemars",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"sync_lib",
|
"sync_lib",
|
||||||
|
|
@ -2589,6 +2352,7 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"ts-rs",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2622,6 +2386,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "test-case"
|
name = "test-case"
|
||||||
version = "3.3.1"
|
version = "3.3.1"
|
||||||
|
|
@ -2712,6 +2485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2920,6 +2694,31 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ts-rs"
|
||||||
|
version = "10.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"lazy_static",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"ts-rs-macros",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ts-rs-macros"
|
||||||
|
version = "10.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.90",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ inefficient_to_string = "warn"
|
||||||
linkedlist = "warn"
|
linkedlist = "warn"
|
||||||
lossy_float_literal = "warn"
|
lossy_float_literal = "warn"
|
||||||
macro_use_imports = "warn"
|
macro_use_imports = "warn"
|
||||||
match_on_vec_items = "warn"
|
|
||||||
match_wildcard_for_single_variants = "warn"
|
match_wildcard_for_single_variants = "warn"
|
||||||
mem_forget = "warn"
|
mem_forget = "warn"
|
||||||
needless_borrow = "warn"
|
needless_borrow = "warn"
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
database:
|
database:
|
||||||
databases_directory_path: databases
|
databases_directory_path: databases
|
||||||
max_connections_per_vault: 12
|
max_connections_per_vault: 12
|
||||||
|
cursor_timeout_seconds: 60
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
port: 3000
|
port: 3000
|
||||||
max_body_size_mb: 512
|
max_body_size_mb: 512
|
||||||
max_clients_per_vault: 256
|
max_clients_per_vault: 256
|
||||||
response_timeout_seconds: 60
|
response_timeout_seconds: 60
|
||||||
|
|
||||||
users:
|
users:
|
||||||
user_configs:
|
user_configs:
|
||||||
- name: admin
|
- name: admin
|
||||||
token: test-token-change-me
|
token: test-token-change-me
|
||||||
vault_access:
|
vault_access:
|
||||||
type: allow_access_to_all
|
type: allow_access_to_all
|
||||||
|
- name: other-admin
|
||||||
- name: other-admin
|
token: test-token-change-me2
|
||||||
token: test-token-change-me2
|
vault_access:
|
||||||
vault_access:
|
type: allow_access_to_all
|
||||||
type: allow_access_to_all
|
- name: test
|
||||||
|
token: other-test-token
|
||||||
- name: test
|
vault_access:
|
||||||
token: other-test-token
|
type: allow_list
|
||||||
vault_access:
|
allowed:
|
||||||
type: allow_list
|
- default
|
||||||
allowed:
|
|
||||||
- default
|
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,11 @@ where
|
||||||
self.shift -= *deleted_character_count as i64;
|
self.shift -= *deleted_character_count as i64;
|
||||||
self.last_operation = None;
|
self.last_operation = None;
|
||||||
}
|
}
|
||||||
} else if let Operation::Insert { .. } = last_operation {
|
} else if let Operation::Insert { .. } = last_operation
|
||||||
if threshold_index + self.shift - last_operation.len() as i64
|
&& threshold_index + self.shift - last_operation.len() as i64
|
||||||
> last_operation.end_index() as i64
|
> last_operation.end_index() as i64
|
||||||
{
|
{
|
||||||
self.last_operation = None;
|
self.last_operation = None;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly-2025-03-14"
|
channel = "nightly-2025-06-06"
|
||||||
targets = [ "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl" ]
|
targets = [ "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl" ]
|
||||||
profile = "default"
|
profile = "default"
|
||||||
|
|
|
||||||
|
|
@ -18,25 +18,23 @@ log = { version = "0.4.27" }
|
||||||
anyhow = { version = "1.0.98", features = ["backtrace"] }
|
anyhow = { version = "1.0.98", features = ["backtrace"] }
|
||||||
axum = { version = "0.7.4", features = ["ws", "macros", "tracing", "multipart"]}
|
axum = { version = "0.7.4", features = ["ws", "macros", "tracing", "multipart"]}
|
||||||
axum-extra = { version = "0.9.6", features = ["typed-header"] }
|
axum-extra = { version = "0.9.6", features = ["typed-header"] }
|
||||||
aide-axum-typed-multipart = "0.13.0"
|
|
||||||
axum_typed_multipart = "0.11.0"
|
axum_typed_multipart = "0.11.0"
|
||||||
tower-http = { version = "0.6.1", features = ["cors", "trace", "limit", "timeout"] }
|
tower-http = { version = "0.6.1", features = ["cors", "trace", "limit", "timeout"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"]}
|
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"]}
|
||||||
serde_yaml = "0.9.34"
|
|
||||||
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] }
|
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] }
|
||||||
chrono = { version = "0.4.41", features = ["serde"] }
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
aide = { version = "0.13.5", features = ["axum", "axum-ws", "scalar", "axum-headers"] }
|
|
||||||
schemars = { version = "0.8.22", features = ["chrono", "uuid1", "bytes"] }
|
|
||||||
tracing = "0.1.41"
|
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
sanitize-filename = "0.6.0"
|
sanitize-filename = "0.6.0"
|
||||||
axum-jsonschema = { version = "0.8.0", features = ["aide"] }
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
clap = { version = "4.5.38", features = ["derive"] }
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
clap-verbosity-flag = "3.0.3"
|
clap-verbosity-flag = "3.0.3"
|
||||||
bimap = "0.6.3"
|
bimap = "0.6.3"
|
||||||
|
ts-rs = { version = "10.1", features = ["uuid-impl", "chrono-impl"] }
|
||||||
|
serde_with = "3.12.0"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
pub mod broadcasts;
|
pub mod cursors;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
|
pub mod websocket;
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use broadcasts::Broadcasts;
|
use cursors::Cursors;
|
||||||
use database::Database;
|
use database::Database;
|
||||||
|
use websocket::broadcasts::Broadcasts;
|
||||||
|
|
||||||
use crate::{config::Config, consts::DEFAULT_CONFIG_PATH};
|
use crate::{config::Config, consts::DEFAULT_CONFIG_PATH};
|
||||||
|
|
||||||
|
|
@ -13,6 +15,7 @@ use crate::{config::Config, consts::DEFAULT_CONFIG_PATH};
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub database: Database,
|
pub database: Database,
|
||||||
|
pub cursors: Cursors,
|
||||||
pub broadcasts: Broadcasts,
|
pub broadcasts: Broadcasts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,12 +25,16 @@ impl AppState {
|
||||||
let path = std::path::PathBuf::from(config_path);
|
let path = std::path::PathBuf::from(config_path);
|
||||||
|
|
||||||
let config = Config::read_or_create(&path).await?;
|
let config = Config::read_or_create(&path).await?;
|
||||||
let database = Database::try_new(&config.database).await?;
|
|
||||||
let broadcasts = Broadcasts::new(&config.server);
|
let broadcasts = Broadcasts::new(&config.server);
|
||||||
|
let database = Database::try_new(&config.database, &broadcasts).await?;
|
||||||
|
let cursors: Cursors = Cursors::new(&config.database, &broadcasts);
|
||||||
|
|
||||||
|
Cursors::start_background_task(cursors.clone());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
database,
|
database,
|
||||||
|
cursors,
|
||||||
broadcasts,
|
broadcasts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
128
backend/sync_server/src/app_state/cursors.rs
Normal file
128
backend/sync_server/src/app_state/cursors.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
use core::time::Duration;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
database::models::{DeviceId, VaultId},
|
||||||
|
websocket::{
|
||||||
|
broadcasts::Broadcasts,
|
||||||
|
models::{
|
||||||
|
ClientCursors, CursorPositionFromServer, CursorSpan, WebSocketServerMessage,
|
||||||
|
WebSocketServerMessageWithOrigin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crate::config::database_config::DatabaseConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Cursors {
|
||||||
|
config: DatabaseConfig,
|
||||||
|
broadcasts: Broadcasts,
|
||||||
|
vault_to_cursors: Arc<Mutex<HashMap<VaultId, Vec<ClientCursorsWithTimeToLive>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursors {
|
||||||
|
pub fn new(config: &DatabaseConfig, broadcasts: &Broadcasts) -> Self {
|
||||||
|
Self {
|
||||||
|
config: config.clone(),
|
||||||
|
broadcasts: broadcasts.clone(),
|
||||||
|
vault_to_cursors: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_cursors(
|
||||||
|
&self,
|
||||||
|
vault_id: VaultId,
|
||||||
|
user_name: String,
|
||||||
|
device_id: &DeviceId,
|
||||||
|
document_to_cursors: HashMap<String, Vec<CursorSpan>>,
|
||||||
|
) {
|
||||||
|
let mut vault_to_cursors = self.vault_to_cursors.lock().await;
|
||||||
|
|
||||||
|
let all_device_cursors = vault_to_cursors.entry(vault_id).or_insert_with(Vec::new);
|
||||||
|
|
||||||
|
all_device_cursors.retain(|c| &c.client_cursors.device_id != device_id);
|
||||||
|
all_device_cursors.push(ClientCursorsWithTimeToLive::new(ClientCursors {
|
||||||
|
user_name,
|
||||||
|
device_id: device_id.to_string(),
|
||||||
|
cursors: document_to_cursors,
|
||||||
|
}));
|
||||||
|
|
||||||
|
drop(vault_to_cursors); // Explicitly drop the lock before broadcasting to avoid deadlock
|
||||||
|
self.broadcast_cursors().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cursors(&self, vault_id: &VaultId) -> Vec<ClientCursors> {
|
||||||
|
let vault_to_cursors = self.vault_to_cursors.lock().await;
|
||||||
|
vault_to_cursors
|
||||||
|
.get(vault_id)
|
||||||
|
.map(|cursors| {
|
||||||
|
cursors
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|with_ttl| with_ttl.client_cursors)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_background_task(self) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
self.remove_expired_cursors().await;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_expired_cursors(&self) {
|
||||||
|
let mut vault_to_cursors = self.vault_to_cursors.lock().await;
|
||||||
|
|
||||||
|
for (_vault_id, cursors) in vault_to_cursors.iter_mut() {
|
||||||
|
cursors.retain(|cursor| !cursor.is_expired(self.config.cursor_timeout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn broadcast_cursors(&self) {
|
||||||
|
let vault_to_cursors = self.vault_to_cursors.lock().await;
|
||||||
|
|
||||||
|
for (vault_id, cursors) in vault_to_cursors.iter() {
|
||||||
|
self.broadcasts
|
||||||
|
.send_document_update(
|
||||||
|
vault_id.clone(),
|
||||||
|
WebSocketServerMessageWithOrigin::new(WebSocketServerMessage::CursorPositions(
|
||||||
|
CursorPositionFromServer {
|
||||||
|
clients: cursors.iter().map(|c| c.client_cursors.clone()).collect(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_cursors_of_device(&self, vault_id: &str, device_id: &str) {
|
||||||
|
let mut vault_to_cursors = self.vault_to_cursors.lock().await;
|
||||||
|
|
||||||
|
if let Some(cursors) = vault_to_cursors.get_mut(vault_id) {
|
||||||
|
cursors.retain(|c| c.client_cursors.device_id != device_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ClientCursorsWithTimeToLive {
|
||||||
|
client_cursors: ClientCursors,
|
||||||
|
last_updated: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientCursorsWithTimeToLive {
|
||||||
|
fn new(client_cursors: ClientCursors) -> Self {
|
||||||
|
Self {
|
||||||
|
client_cursors,
|
||||||
|
last_updated: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_expired(&self, ttl: Duration) -> bool { self.last_updated.elapsed() > ttl }
|
||||||
|
}
|
||||||
|
|
@ -6,23 +6,29 @@ use models::{
|
||||||
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId,
|
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId,
|
||||||
};
|
};
|
||||||
use sqlx::{sqlite::SqliteConnectOptions, types::chrono::Utc};
|
use sqlx::{sqlite::SqliteConnectOptions, types::chrono::Utc};
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
use sqlx::{Pool, Sqlite, sqlite::SqlitePoolOptions};
|
use sqlx::{Pool, Sqlite, sqlite::SqlitePoolOptions};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use uuid::fmt::Hyphenated;
|
use uuid::fmt::Hyphenated;
|
||||||
|
|
||||||
|
use super::websocket::{
|
||||||
|
broadcasts::Broadcasts,
|
||||||
|
models::{WebSocketServerMessage, WebSocketServerMessageWithOrigin, WebSocketVaultUpdate},
|
||||||
|
};
|
||||||
use crate::config::database_config::DatabaseConfig;
|
use crate::config::database_config::DatabaseConfig;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
config: DatabaseConfig,
|
config: DatabaseConfig,
|
||||||
|
broadcasts: Broadcasts,
|
||||||
connection_pools: Arc<Mutex<HashMap<VaultId, Pool<Sqlite>>>>,
|
connection_pools: Arc<Mutex<HashMap<VaultId, Pool<Sqlite>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Transaction<'a> = sqlx::Transaction<'a, Sqlite>;
|
pub type Transaction<'a> = sqlx::Transaction<'a, Sqlite>;
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn try_new(config: &DatabaseConfig) -> Result<Self> {
|
pub async fn try_new(config: &DatabaseConfig, broadcasts: &Broadcasts) -> Result<Self> {
|
||||||
tokio::fs::create_dir_all(&config.databases_directory_path)
|
tokio::fs::create_dir_all(&config.databases_directory_path)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
|
|
@ -55,6 +61,7 @@ impl Database {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
connection_pools: Arc::new(Mutex::new(connection_pools)),
|
connection_pools: Arc::new(Mutex::new(connection_pools)),
|
||||||
|
broadcasts: broadcasts.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,7 +369,7 @@ impl Database {
|
||||||
|
|
||||||
pub async fn insert_document_version(
|
pub async fn insert_document_version(
|
||||||
&self,
|
&self,
|
||||||
vault: &VaultId,
|
vault_id: &VaultId,
|
||||||
version: &StoredDocumentVersion,
|
version: &StoredDocumentVersion,
|
||||||
transaction: Option<&mut Transaction<'_>>,
|
transaction: Option<&mut Transaction<'_>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -394,10 +401,25 @@ impl Database {
|
||||||
if let Some(transaction) = transaction {
|
if let Some(transaction) = transaction {
|
||||||
query.execute(&mut **transaction).await
|
query.execute(&mut **transaction).await
|
||||||
} else {
|
} else {
|
||||||
query.execute(&self.get_connection_pool(vault).await?).await
|
query
|
||||||
|
.execute(&self.get_connection_pool(vault_id).await?)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
.context("Cannot insert document version")?;
|
.context("Cannot insert document version")?;
|
||||||
|
|
||||||
|
self.broadcasts
|
||||||
|
.send_document_update(
|
||||||
|
vault_id.clone(),
|
||||||
|
WebSocketServerMessageWithOrigin::with_origin(
|
||||||
|
version.device_id.clone(),
|
||||||
|
WebSocketServerMessage::VaultUpdate(WebSocketVaultUpdate {
|
||||||
|
documents: vec![version.clone().into()],
|
||||||
|
is_initial_sync: false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sync_lib::bytes_to_base64;
|
use sync_lib::bytes_to_base64;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub type VaultId = String;
|
pub type VaultId = String;
|
||||||
pub type VaultUpdateId = i64;
|
pub type VaultUpdateId = i64;
|
||||||
|
|
||||||
pub type DocumentId = uuid::Uuid;
|
pub type DocumentId = uuid::Uuid;
|
||||||
pub type UserId = String;
|
pub type UserId = String;
|
||||||
pub type DeviceId = String;
|
pub type DeviceId = String;
|
||||||
|
|
@ -25,16 +26,20 @@ impl PartialEq<Self> for StoredDocumentVersion {
|
||||||
fn eq(&self, other: &Self) -> bool { self.vault_update_id == other.vault_update_id }
|
fn eq(&self, other: &Self) -> bool { self.vault_update_id == other.vault_update_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DocumentVersionWithoutContent {
|
pub struct DocumentVersionWithoutContent {
|
||||||
|
#[ts(as = "i32")]
|
||||||
pub vault_update_id: VaultUpdateId,
|
pub vault_update_id: VaultUpdateId,
|
||||||
|
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub updated_date: DateTime<Utc>,
|
pub updated_date: DateTime<Utc>,
|
||||||
pub is_deleted: bool,
|
pub is_deleted: bool,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub device_id: DeviceId,
|
pub device_id: DeviceId,
|
||||||
|
|
||||||
|
#[ts(as = "i32")]
|
||||||
pub content_size: u64,
|
pub content_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,10 +58,12 @@ impl From<StoredDocumentVersion> for DocumentVersionWithoutContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DocumentVersion {
|
pub struct DocumentVersion {
|
||||||
|
#[ts(as = "i32")]
|
||||||
pub vault_update_id: VaultUpdateId,
|
pub vault_update_id: VaultUpdateId,
|
||||||
|
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub updated_date: DateTime<Utc>,
|
pub updated_date: DateTime<Utc>,
|
||||||
|
|
|
||||||
3
backend/sync_server/src/app_state/websocket.rs
Normal file
3
backend/sync_server/src/app_state/websocket.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod broadcasts;
|
||||||
|
pub mod models;
|
||||||
|
pub mod utils;
|
||||||
|
|
@ -3,19 +3,15 @@ use std::{collections::HashMap, sync::Arc};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use tokio::sync::{Mutex, broadcast};
|
use tokio::sync::{Mutex, broadcast};
|
||||||
|
|
||||||
use super::database::models::{DeviceId, DocumentVersionWithoutContent, VaultId};
|
use super::models::WebSocketServerMessageWithOrigin;
|
||||||
use crate::{config::server_config::ServerConfig, errors::server_error};
|
use crate::{
|
||||||
|
app_state::database::models::VaultId, config::server_config::ServerConfig, errors::server_error,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Broadcasts {
|
pub struct Broadcasts {
|
||||||
max_clients_per_vault: usize,
|
max_clients_per_vault: usize,
|
||||||
tx: Arc<Mutex<HashMap<VaultId, broadcast::Sender<VaultUpdate>>>>,
|
tx: Arc<Mutex<HashMap<VaultId, broadcast::Sender<WebSocketServerMessageWithOrigin>>>>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VaultUpdate {
|
|
||||||
pub origin_device_id: Option<DeviceId>,
|
|
||||||
pub document: DocumentVersionWithoutContent,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Broadcasts {
|
impl Broadcasts {
|
||||||
|
|
@ -26,20 +22,27 @@ impl Broadcasts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_receiver(&self, vault: VaultId) -> broadcast::Receiver<VaultUpdate> {
|
pub async fn get_receiver(
|
||||||
|
&self,
|
||||||
|
vault: VaultId,
|
||||||
|
) -> broadcast::Receiver<WebSocketServerMessageWithOrigin> {
|
||||||
let tx = self.get_or_create(vault).await;
|
let tx = self.get_or_create(vault).await;
|
||||||
|
|
||||||
tx.subscribe()
|
tx.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sent a document update to all clients subscribed to the vault.
|
/// Notify all clients (who are subscribed to the vault) about an update.
|
||||||
/// We ignore & log failures.
|
/// We only log failures.
|
||||||
pub async fn send(&self, vault: VaultId, document: VaultUpdate) {
|
pub async fn send_document_update(
|
||||||
|
&self,
|
||||||
|
vault: VaultId,
|
||||||
|
document: WebSocketServerMessageWithOrigin,
|
||||||
|
) {
|
||||||
let tx = self.get_or_create(vault).await;
|
let tx = self.get_or_create(vault).await;
|
||||||
|
|
||||||
let result = tx
|
let result = tx
|
||||||
.send(document)
|
.send(document)
|
||||||
.context("Cannot broadcast update message to websocket listeners")
|
.context("Cannot broadcast server message to websocket listeners")
|
||||||
.map_err(server_error);
|
.map_err(server_error);
|
||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
|
|
@ -47,7 +50,10 @@ impl Broadcasts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_or_create(&self, vault: VaultId) -> broadcast::Sender<VaultUpdate> {
|
async fn get_or_create(
|
||||||
|
&self,
|
||||||
|
vault: VaultId,
|
||||||
|
) -> broadcast::Sender<WebSocketServerMessageWithOrigin> {
|
||||||
let mut tx = self.tx.lock().await;
|
let mut tx = self.tx.lock().await;
|
||||||
|
|
||||||
tx.entry(vault)
|
tx.entry(vault)
|
||||||
88
backend/sync_server/src/app_state/websocket/models.rs
Normal file
88
backend/sync_server/src/app_state/websocket/models.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use crate::app_state::database::models::{DeviceId, DocumentVersionWithoutContent, VaultUpdateId};
|
||||||
|
|
||||||
|
#[derive(TS, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WebSocketHandshake {
|
||||||
|
pub token: String,
|
||||||
|
pub device_id: DeviceId,
|
||||||
|
|
||||||
|
#[ts(as = "Option<i32>")]
|
||||||
|
pub last_seen_vault_update_id: Option<VaultUpdateId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CursorSpan {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CursorPositionFromClient {
|
||||||
|
pub document_to_cursors: HashMap<String, Vec<CursorSpan>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ClientCursors {
|
||||||
|
pub user_name: String,
|
||||||
|
pub device_id: DeviceId,
|
||||||
|
pub cursors: HashMap<String, Vec<CursorSpan>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CursorPositionFromServer {
|
||||||
|
pub clients: Vec<ClientCursors>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WebSocketVaultUpdate {
|
||||||
|
pub documents: Vec<DocumentVersionWithoutContent>,
|
||||||
|
pub is_initial_sync: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum WebSocketClientMessage {
|
||||||
|
Handshake(WebSocketHandshake),
|
||||||
|
CursorPositions(CursorPositionFromClient),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum WebSocketServerMessage {
|
||||||
|
VaultUpdate(WebSocketVaultUpdate),
|
||||||
|
CursorPositions(CursorPositionFromServer),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WebSocketServerMessageWithOrigin {
|
||||||
|
pub origin_device_id: Option<DeviceId>,
|
||||||
|
pub message: WebSocketServerMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSocketServerMessageWithOrigin {
|
||||||
|
pub fn new(message: WebSocketServerMessage) -> Self {
|
||||||
|
Self {
|
||||||
|
origin_device_id: None,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_origin(origin_device_id: DeviceId, message: WebSocketServerMessage) -> Self {
|
||||||
|
Self {
|
||||||
|
origin_device_id: Some(origin_device_id),
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
backend/sync_server/src/app_state/websocket/utils.rs
Normal file
80
backend/sync_server/src/app_state/websocket/utils.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
|
use futures::{sink::SinkExt, stream::SplitSink};
|
||||||
|
|
||||||
|
use super::models::{WebSocketClientMessage, WebSocketHandshake, WebSocketServerMessage};
|
||||||
|
use crate::{
|
||||||
|
app_state::{
|
||||||
|
AppState,
|
||||||
|
database::models::{DocumentVersionWithoutContent, VaultId, VaultUpdateId},
|
||||||
|
},
|
||||||
|
config::user_config::User,
|
||||||
|
errors::{SyncServerError, server_error, unauthenticated_error},
|
||||||
|
server::auth::auth,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AuthenticatedWebSocketHandshake {
|
||||||
|
pub handshake: WebSocketHandshake,
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_authenticated_handshake(
|
||||||
|
state: &AppState,
|
||||||
|
vault_id: &VaultId,
|
||||||
|
message: Option<Message>,
|
||||||
|
) -> Result<AuthenticatedWebSocketHandshake, SyncServerError> {
|
||||||
|
if let Some(Message::Text(message)) = message {
|
||||||
|
let message: WebSocketClientMessage = serde_json::from_str(&message)
|
||||||
|
.context("Failed to parse message")
|
||||||
|
.map_err(server_error)?;
|
||||||
|
|
||||||
|
match message {
|
||||||
|
WebSocketClientMessage::Handshake(handshake) => {
|
||||||
|
let user = auth(state, handshake.token.trim(), vault_id)?;
|
||||||
|
Ok(AuthenticatedWebSocketHandshake { handshake, user })
|
||||||
|
}
|
||||||
|
WebSocketClientMessage::CursorPositions(_) => Err(unauthenticated_error(
|
||||||
|
anyhow::anyhow!("Expected a handshake message"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(unauthenticated_error(anyhow::anyhow!(
|
||||||
|
"Failed to authenticate due to invalid message"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_unseen_documents(
|
||||||
|
state: &AppState,
|
||||||
|
vault_id: &VaultId,
|
||||||
|
last_seen_vault_update_id: Option<VaultUpdateId>,
|
||||||
|
) -> Result<Vec<DocumentVersionWithoutContent>, SyncServerError> {
|
||||||
|
if let Some(update_id) = last_seen_vault_update_id {
|
||||||
|
state
|
||||||
|
.database
|
||||||
|
.get_latest_documents_since(vault_id, update_id, None)
|
||||||
|
.await
|
||||||
|
.map_err(server_error)
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
.database
|
||||||
|
.get_latest_documents(vault_id, None)
|
||||||
|
.await
|
||||||
|
.map_err(server_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_update_over_websocket(
|
||||||
|
update: &WebSocketServerMessage,
|
||||||
|
sender: &mut SplitSink<WebSocket, Message>,
|
||||||
|
) -> Result<(), SyncServerError> {
|
||||||
|
let serialized_update = serde_json::to_string(update)
|
||||||
|
.context("Failed to serialize update")
|
||||||
|
.map_err(server_error)?;
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(Message::Text(serialized_update))
|
||||||
|
.await
|
||||||
|
.context("Failed to send message over websocket")
|
||||||
|
.map_err(server_error)
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use database_config::DatabaseConfig;
|
use database_config::DatabaseConfig;
|
||||||
use log::{info, warn};
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use server_config::ServerConfig;
|
use server_config::ServerConfig;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
@ -24,21 +24,23 @@ pub struct Config {
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub async fn read_or_create(path: &Path) -> Result<Self> {
|
pub async fn read_or_create(path: &Path) -> Result<Self> {
|
||||||
if path.exists() {
|
let config = if path.exists() {
|
||||||
info!(
|
info!(
|
||||||
"Loading configuration from '{}'",
|
"Loading configuration from '{}'",
|
||||||
path.canonicalize().unwrap().display()
|
path.canonicalize().unwrap().display()
|
||||||
);
|
);
|
||||||
Self::load_from_file(path).await
|
Self::load_from_file(path).await?
|
||||||
} else {
|
} else {
|
||||||
let config = Self::default();
|
Self::default()
|
||||||
config.write(path).await?;
|
};
|
||||||
warn!(
|
|
||||||
"Configuration file not found, wrote default configuration to '{}'",
|
config.write(path).await?;
|
||||||
path.canonicalize().unwrap().display()
|
info!(
|
||||||
);
|
"Updated configuration at '{}'",
|
||||||
Ok(config)
|
path.canonicalize().unwrap().display()
|
||||||
}
|
);
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_from_file(path: &Path) -> Result<Self> {
|
pub async fn load_from_file(path: &Path) -> Result<Self> {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::consts::{DEFAULT_DATABASES_DIRECTORY_PATH, DEFAULT_MAX_CONNECTIONS_PER_VAULT};
|
use crate::consts::{
|
||||||
|
DEFAULT_CURSOR_TIMEOUT, DEFAULT_DATABASES_DIRECTORY_PATH, DEFAULT_MAX_CONNECTIONS_PER_VAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[serde_with::serde_as]
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct DatabaseConfig {
|
pub struct DatabaseConfig {
|
||||||
#[serde(default = "default_databases_directory_path")]
|
#[serde(default = "default_databases_directory_path")]
|
||||||
|
|
@ -12,6 +16,10 @@ pub struct DatabaseConfig {
|
||||||
|
|
||||||
#[serde(default = "default_max_connections_per_vault")]
|
#[serde(default = "default_max_connections_per_vault")]
|
||||||
pub max_connections_per_vault: u32,
|
pub max_connections_per_vault: u32,
|
||||||
|
|
||||||
|
#[serde(default = "default_cursor_timeout", rename = "cursor_timeout_seconds")]
|
||||||
|
#[serde_as(as = "serde_with::DurationSeconds<u64>")]
|
||||||
|
pub cursor_timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_databases_directory_path() -> PathBuf {
|
fn default_databases_directory_path() -> PathBuf {
|
||||||
|
|
@ -24,11 +32,17 @@ fn default_max_connections_per_vault() -> u32 {
|
||||||
DEFAULT_MAX_CONNECTIONS_PER_VAULT
|
DEFAULT_MAX_CONNECTIONS_PER_VAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_cursor_timeout() -> Duration {
|
||||||
|
debug!("Using default cursor timeout: {DEFAULT_CURSOR_TIMEOUT:?}");
|
||||||
|
DEFAULT_CURSOR_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for DatabaseConfig {
|
impl Default for DatabaseConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
databases_directory_path: default_databases_directory_path(),
|
databases_directory_path: default_databases_directory_path(),
|
||||||
max_connections_per_vault: default_max_connections_per_vault(),
|
max_connections_per_vault: default_max_connections_per_vault(),
|
||||||
|
cursor_timeout: default_cursor_timeout(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_PATH: &str = "config.yml";
|
pub const DEFAULT_CONFIG_PATH: &str = "config.yml";
|
||||||
|
|
||||||
pub const DEFAULT_DATABASES_DIRECTORY_PATH: &str = "databases";
|
pub const DEFAULT_DATABASES_DIRECTORY_PATH: &str = "databases";
|
||||||
|
pub const DEFAULT_MAX_CONNECTIONS_PER_VAULT: u32 = 12;
|
||||||
|
pub const DEFAULT_CURSOR_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
pub const DEFAULT_HOST: &str = "127.0.0.1";
|
pub const DEFAULT_HOST: &str = "127.0.0.1";
|
||||||
pub const DEFAULT_PORT: u16 = 3000;
|
pub const DEFAULT_PORT: u16 = 3000;
|
||||||
pub const DEFAULT_MAX_CONNECTIONS_PER_VAULT: u32 = 12;
|
|
||||||
pub const DEFAULT_MAX_BODY_SIZE_MB: usize = 4096;
|
pub const DEFAULT_MAX_BODY_SIZE_MB: usize = 4096;
|
||||||
pub const DEFAULT_RESPONSE_TIMEOUT_SECONDS: u64 = 60;
|
pub const DEFAULT_RESPONSE_TIMEOUT_SECONDS: u64 = 60;
|
||||||
pub const DEFAULT_MAX_CLIENTS_PER_VAULT: usize = 256;
|
pub const DEFAULT_MAX_CLIENTS_PER_VAULT: usize = 256;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use aide::OperationOutput;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{debug, error};
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum SyncServerError {
|
pub enum SyncServerError {
|
||||||
|
|
@ -45,8 +44,11 @@ impl SyncServerError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct SerializedError {
|
pub struct SerializedError {
|
||||||
|
pub error_type: &'static str,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub causes: Vec<String>,
|
pub causes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -90,41 +92,49 @@ impl From<&anyhow::Error> for SerializedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializedError {
|
SerializedError {
|
||||||
|
error_type: error.downcast_ref::<SyncServerError>().map_or(
|
||||||
|
"UnknownError",
|
||||||
|
|e| match e {
|
||||||
|
SyncServerError::InitError(_) => "InitError",
|
||||||
|
SyncServerError::ClientError(_) => "ClientError",
|
||||||
|
SyncServerError::ServerError(_) => "ServerError",
|
||||||
|
SyncServerError::NotFound(_) => "NotFound",
|
||||||
|
SyncServerError::Unauthenticated(_) => "Unauthenticated",
|
||||||
|
SyncServerError::PermissionDeniedError(_) => "PermissionDeniedError",
|
||||||
|
},
|
||||||
|
),
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
causes,
|
causes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OperationOutput for SyncServerError {
|
pub fn init_error(error: anyhow::Error) -> SyncServerError {
|
||||||
type Inner = Self;
|
debug!("Initialization error: {error:?}");
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn init_error(error: anyhow::Error) -> SyncServerError {
|
|
||||||
SyncServerError::InitError(error)
|
SyncServerError::InitError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_error(error: anyhow::Error) -> SyncServerError {
|
pub fn server_error(error: anyhow::Error) -> SyncServerError {
|
||||||
error!("Server error: {error:?}");
|
debug!("Server error: {error:?}");
|
||||||
SyncServerError::ServerError(error)
|
SyncServerError::ServerError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_error(error: anyhow::Error) -> SyncServerError {
|
pub fn client_error(error: anyhow::Error) -> SyncServerError {
|
||||||
info!("Client error: {error:?}");
|
debug!("Client error: {error:?}");
|
||||||
SyncServerError::ClientError(error)
|
SyncServerError::ClientError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found_error(error: anyhow::Error) -> SyncServerError {
|
pub fn not_found_error(error: anyhow::Error) -> SyncServerError {
|
||||||
info!("Not found: {error:?}");
|
debug!("Not found: {error:?}");
|
||||||
SyncServerError::NotFound(error)
|
SyncServerError::NotFound(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unauthenticated_error(error: anyhow::Error) -> SyncServerError {
|
pub fn unauthenticated_error(error: anyhow::Error) -> SyncServerError {
|
||||||
info!("Unauthenticated user: {error:?}");
|
debug!("Unauthenticated user: {error:?}");
|
||||||
SyncServerError::Unauthenticated(error)
|
SyncServerError::Unauthenticated(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn permission_denied_error(error: anyhow::Error) -> SyncServerError {
|
pub fn permission_denied_error(error: anyhow::Error) -> SyncServerError {
|
||||||
info!("Permission denied: {error:?}");
|
debug!("Permission denied: {error:?}");
|
||||||
SyncServerError::PermissionDeniedError(error)
|
SyncServerError::PermissionDeniedError(error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mod auth;
|
pub mod auth;
|
||||||
mod create_document;
|
mod create_document;
|
||||||
mod delete_document;
|
mod delete_document;
|
||||||
mod device_id_header;
|
mod device_id_header;
|
||||||
|
|
@ -6,35 +6,27 @@ mod fetch_document_version;
|
||||||
mod fetch_document_version_content;
|
mod fetch_document_version_content;
|
||||||
mod fetch_latest_document_version;
|
mod fetch_latest_document_version;
|
||||||
mod fetch_latest_documents;
|
mod fetch_latest_documents;
|
||||||
|
mod index;
|
||||||
mod ping;
|
mod ping;
|
||||||
mod requests;
|
mod requests;
|
||||||
mod responses;
|
mod responses;
|
||||||
mod update_document;
|
mod update_document;
|
||||||
mod websocket;
|
mod websocket;
|
||||||
|
|
||||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
use std::{ffi::OsString, time::Duration};
|
||||||
|
|
||||||
use aide::{
|
|
||||||
axum::{
|
|
||||||
ApiRouter,
|
|
||||||
routing::{delete, get, post, put},
|
|
||||||
},
|
|
||||||
openapi::{Info, OpenApi},
|
|
||||||
scalar::Scalar,
|
|
||||||
transform::TransformOpenApi,
|
|
||||||
};
|
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use auth::auth_middleware;
|
use auth::auth_middleware;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json,
|
Router,
|
||||||
extract::{DefaultBodyLimit, Request},
|
extract::{DefaultBodyLimit, Request},
|
||||||
http::{self, HeaderValue, Method},
|
http::{self, HeaderValue, Method},
|
||||||
middleware,
|
middleware,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::IntoMakeService,
|
routing::{IntoMakeService, delete, get, post, put},
|
||||||
};
|
};
|
||||||
use device_id_header::DEVICE_ID_HEADER_NAME;
|
use device_id_header::DEVICE_ID_HEADER_NAME;
|
||||||
use log::{error, info};
|
use log::info;
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
LatencyUnit,
|
LatencyUnit,
|
||||||
|
|
@ -51,26 +43,21 @@ use tracing::{Level, info_span};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::AppState,
|
app_state::AppState,
|
||||||
config::server_config::ServerConfig,
|
config::server_config::ServerConfig,
|
||||||
errors::{SerializedError, client_error, not_found_error},
|
errors::{client_error, not_found_error},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
|
pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
|
||||||
aide::r#gen::on_error(|err| error!("{err}"));
|
|
||||||
aide::r#gen::extract_schemas(true);
|
|
||||||
|
|
||||||
let app_state = AppState::try_new(config_path)
|
let app_state = AppState::try_new(config_path)
|
||||||
.await
|
.await
|
||||||
.context("Failed to initialise app state")?;
|
.context("Failed to initialise app state")?;
|
||||||
|
|
||||||
let server_config = app_state.config.server.clone();
|
let server_config = app_state.config.server.clone();
|
||||||
|
|
||||||
let mut api = create_open_api();
|
let app = Router::new()
|
||||||
let app = ApiRouter::new()
|
|
||||||
.nest("/", get_authed_routes(app_state.clone()))
|
.nest("/", get_authed_routes(app_state.clone()))
|
||||||
.api_route("/vaults/:vault_id/ping", get(ping::ping))
|
.route("/", get(index::index))
|
||||||
|
.route("/vaults/:vault_id/ping", get(ping::ping))
|
||||||
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler))
|
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler))
|
||||||
.route("/", Scalar::new("/api.json").axum_route())
|
|
||||||
.route("/api.json", axum::routing::get(serve_api))
|
|
||||||
.layer(DefaultBodyLimit::disable())
|
.layer(DefaultBodyLimit::disable())
|
||||||
.layer(RequestBodyLimitLayer::new(
|
.layer(RequestBodyLimitLayer::new(
|
||||||
app_state.config.server.max_body_size_mb * 1024 * 1024,
|
app_state.config.server.max_body_size_mb * 1024 * 1024,
|
||||||
|
|
@ -108,8 +95,6 @@ pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
|
||||||
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
|
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
|
||||||
)
|
)
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
.finish_api_with(&mut api, add_api_docs_error_example)
|
|
||||||
.layer(Extension(Arc::new(api))) // https://github.com/tamasfe/aide/blob/507f4a8822bc0c13cbda0f589da1e0f4cbcdb812/examples/example-axum/src/main.rs#L39
|
|
||||||
.fallback(handle_404)
|
.fallback(handle_404)
|
||||||
.fallback(handle_405)
|
.fallback(handle_405)
|
||||||
.into_make_service();
|
.into_make_service();
|
||||||
|
|
@ -117,67 +102,33 @@ pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
|
||||||
start_server(app, &server_config).await
|
start_server(app, &server_config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_api(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoResponse { Json(api) }
|
fn get_authed_routes(app_state: AppState) -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
fn create_open_api() -> OpenApi {
|
.route(
|
||||||
OpenApi {
|
|
||||||
info: Info {
|
|
||||||
title: "VaultLink sync server".to_owned(),
|
|
||||||
summary: Some(
|
|
||||||
"Simple API for syncing documents between concurrent clients.".to_owned(),
|
|
||||||
),
|
|
||||||
description: Some(include_str!("../README.md").to_owned()),
|
|
||||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
|
||||||
..Info::default()
|
|
||||||
},
|
|
||||||
..OpenApi::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_api_docs_error_example(api: TransformOpenApi<'_>) -> TransformOpenApi<'_> {
|
|
||||||
api.default_response_with::<Json<SerializedError>, _>(|res| {
|
|
||||||
res.example(SerializedError {
|
|
||||||
message: "An error has occurred".to_owned(),
|
|
||||||
causes: vec![],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_authed_routes(app_state: AppState) -> ApiRouter<AppState> {
|
|
||||||
ApiRouter::new()
|
|
||||||
.api_route(
|
|
||||||
"/vaults/:vault_id/documents",
|
"/vaults/:vault_id/documents",
|
||||||
get(fetch_latest_documents::fetch_latest_documents),
|
get(fetch_latest_documents::fetch_latest_documents),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents",
|
"/vaults/:vault_id/documents",
|
||||||
post(create_document::create_document_multipart),
|
post(create_document::create_document),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents/json",
|
|
||||||
post(create_document::create_document_json),
|
|
||||||
)
|
|
||||||
.api_route(
|
|
||||||
"/vaults/:vault_id/documents/:document_id",
|
"/vaults/:vault_id/documents/:document_id",
|
||||||
get(fetch_latest_document_version::fetch_latest_document_version),
|
get(fetch_latest_document_version::fetch_latest_document_version),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents/:document_id",
|
"/vaults/:vault_id/documents/:document_id",
|
||||||
put(update_document::update_document_multipart),
|
put(update_document::update_document),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents/:document_id/json",
|
|
||||||
put(update_document::update_document_json),
|
|
||||||
)
|
|
||||||
.api_route(
|
|
||||||
"/vaults/:vault_id/documents/:document_id/versions/:version_id",
|
"/vaults/:vault_id/documents/:document_id/versions/:version_id",
|
||||||
put(fetch_document_version::fetch_document_version),
|
put(fetch_document_version::fetch_document_version),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents/:document_id/versions/:version_id/content",
|
"/vaults/:vault_id/documents/:document_id/versions/:version_id/content",
|
||||||
put(fetch_document_version_content::fetch_document_version_content),
|
put(fetch_document_version_content::fetch_document_version_content),
|
||||||
)
|
)
|
||||||
.api_route(
|
.route(
|
||||||
"/vaults/:vault_id/documents/:document_id",
|
"/vaults/:vault_id/documents/:document_id",
|
||||||
delete(delete_document::delete_document),
|
delete(delete_document::delete_document),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
9
backend/sync_server/src/server/assets/index.html
Normal file
9
backend/sync_server/src/server/assets/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>VaultLink</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>VaultLink server</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -47,19 +47,22 @@ pub fn auth(state: &AppState, token: &str, vault_id: &VaultId) -> Result<User, S
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| unauthenticated_error(anyhow::anyhow!("Invalid token")))?;
|
.ok_or_else(|| unauthenticated_error(anyhow::anyhow!("Invalid token")))?;
|
||||||
|
|
||||||
info!("User `{}` authenticated", user.name);
|
|
||||||
|
|
||||||
if match user.vault_access {
|
if match user.vault_access {
|
||||||
VaultAccess::AllowAccessToAll => true,
|
VaultAccess::AllowAccessToAll => true,
|
||||||
VaultAccess::AllowList(AllowListedVaults { ref allowed }) => allowed.contains(vault_id),
|
VaultAccess::AllowList(AllowListedVaults { ref allowed }) => allowed.contains(vault_id),
|
||||||
} {
|
} {
|
||||||
info!(
|
info!(
|
||||||
"User `{}` is authorised to access to vault `{}`",
|
"User '{}' is authenticated and is authorised to access to vault '{vault_id}'",
|
||||||
user.name, vault_id
|
user.name
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
} else {
|
} else {
|
||||||
|
info!(
|
||||||
|
"User '{}' is authenticated but is not authorised to access vault '{vault_id}'",
|
||||||
|
user.name
|
||||||
|
);
|
||||||
|
|
||||||
Err(permission_denied_error(anyhow::anyhow!(
|
Err(permission_denied_error(anyhow::anyhow!(
|
||||||
"Permission denied for vault `{vault_id}`"
|
"Permission denied for vault `{vault_id}`"
|
||||||
)))
|
)))
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,24 @@
|
||||||
use aide_axum_typed_multipart::TypedMultipart;
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension, Json,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
};
|
};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use axum_jsonschema::Json;
|
use axum_typed_multipart::TypedMultipart;
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sync_lib::base64_to_bytes;
|
|
||||||
|
|
||||||
use super::{
|
use super::{device_id_header::DeviceIdHeader, requests::CreateDocumentVersion};
|
||||||
device_id_header::DeviceIdHeader,
|
|
||||||
requests::{CreateDocumentVersion, CreateDocumentVersionMultipart},
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{
|
app_state::{
|
||||||
AppState,
|
AppState,
|
||||||
broadcasts::VaultUpdate,
|
database::models::{DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
|
||||||
database::models::{
|
|
||||||
DeviceId, DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
config::user_config::User,
|
config::user_config::User,
|
||||||
errors::{SyncServerError, client_error, server_error},
|
errors::{SyncServerError, client_error, server_error},
|
||||||
utils::{normalize::normalize, sanitize_path::sanitize_path},
|
utils::{normalize::normalize, sanitize_path::sanitize_path},
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct CreateDocumentPathParams {
|
pub struct CreateDocumentPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
@ -38,66 +28,12 @@ pub struct CreateDocumentPathParams {
|
||||||
/// already. If a document with the same path exists, a new version is created
|
/// already. If a document with the same path exists, a new version is created
|
||||||
/// with their content merged.
|
/// with their content merged.
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn create_document_multipart(
|
pub async fn create_document(
|
||||||
Path(CreateDocumentPathParams { vault_id }): Path<CreateDocumentPathParams>,
|
Path(CreateDocumentPathParams { vault_id }): Path<CreateDocumentPathParams>,
|
||||||
Extension(user): Extension<User>,
|
Extension(user): Extension<User>,
|
||||||
TypedHeader(user_agent): TypedHeader<DeviceIdHeader>,
|
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart<
|
TypedMultipart(request): TypedMultipart<CreateDocumentVersion>,
|
||||||
CreateDocumentVersionMultipart,
|
|
||||||
>,
|
|
||||||
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
|
||||||
internal_create_document(
|
|
||||||
user,
|
|
||||||
user_agent,
|
|
||||||
state,
|
|
||||||
vault_id,
|
|
||||||
request.document_id,
|
|
||||||
request.relative_path,
|
|
||||||
request.device_id,
|
|
||||||
request.content.contents.to_vec(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new document in case a document with the same doesn't exist
|
|
||||||
/// already. If a document with the same path exists, a new version is created
|
|
||||||
/// with their content merged.
|
|
||||||
#[axum::debug_handler]
|
|
||||||
pub async fn create_document_json(
|
|
||||||
Path(CreateDocumentPathParams { vault_id }): Path<CreateDocumentPathParams>,
|
|
||||||
Extension(user): Extension<User>,
|
|
||||||
TypedHeader(user_agent): TypedHeader<DeviceIdHeader>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Json(request): Json<CreateDocumentVersion>,
|
|
||||||
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
|
||||||
let content_bytes = base64_to_bytes(&request.content_base64)
|
|
||||||
.context("Failed to decode base64 content in request")
|
|
||||||
.map_err(client_error)?;
|
|
||||||
|
|
||||||
internal_create_document(
|
|
||||||
user,
|
|
||||||
user_agent,
|
|
||||||
state,
|
|
||||||
vault_id,
|
|
||||||
request.document_id,
|
|
||||||
request.relative_path,
|
|
||||||
request.device_id,
|
|
||||||
content_bytes,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn internal_create_document(
|
|
||||||
user: User,
|
|
||||||
user_agent: DeviceIdHeader,
|
|
||||||
state: AppState,
|
|
||||||
vault_id: VaultId,
|
|
||||||
document_id: Option<DocumentId>,
|
|
||||||
relative_path: String,
|
|
||||||
device_id: Option<DeviceId>,
|
|
||||||
content: Vec<u8>,
|
|
||||||
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
||||||
let mut transaction = state
|
let mut transaction = state
|
||||||
.database
|
.database
|
||||||
|
|
@ -105,7 +41,7 @@ async fn internal_create_document(
|
||||||
.await
|
.await
|
||||||
.map_err(server_error)?;
|
.map_err(server_error)?;
|
||||||
|
|
||||||
let document_id = match document_id {
|
let document_id = match request.document_id {
|
||||||
Some(document_id) => {
|
Some(document_id) => {
|
||||||
let existing_version = state
|
let existing_version = state
|
||||||
.database
|
.database
|
||||||
|
|
@ -130,17 +66,17 @@ async fn internal_create_document(
|
||||||
.await
|
.await
|
||||||
.map_err(server_error)?;
|
.map_err(server_error)?;
|
||||||
|
|
||||||
let sanitized_relative_path = sanitize_path(&relative_path);
|
let sanitized_relative_path = sanitize_path(&request.relative_path);
|
||||||
|
|
||||||
let new_version = StoredDocumentVersion {
|
let new_version = StoredDocumentVersion {
|
||||||
vault_update_id: last_update_id + 1,
|
vault_update_id: last_update_id + 1,
|
||||||
document_id,
|
document_id,
|
||||||
relative_path: sanitized_relative_path,
|
relative_path: sanitized_relative_path,
|
||||||
content,
|
content: request.content.contents.to_vec(),
|
||||||
updated_date: chrono::Utc::now(),
|
updated_date: chrono::Utc::now(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
user_id: user.name,
|
user_id: user.name,
|
||||||
device_id: user_agent.0,
|
device_id: device_id.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|
@ -155,16 +91,5 @@ async fn internal_create_document(
|
||||||
.context("Failed to commit successful transaction")
|
.context("Failed to commit successful transaction")
|
||||||
.map_err(server_error)?;
|
.map_err(server_error)?;
|
||||||
|
|
||||||
state
|
|
||||||
.broadcasts
|
|
||||||
.send(
|
|
||||||
vault_id,
|
|
||||||
VaultUpdate {
|
|
||||||
origin_device_id: device_id,
|
|
||||||
document: new_version.clone().into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(Json(new_version.into()))
|
Ok(Json(new_version.into()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension, Json,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
};
|
};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use axum_jsonschema::Json;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{device_id_header::DeviceIdHeader, requests::DeleteDocumentVersion};
|
use super::{device_id_header::DeviceIdHeader, requests::DeleteDocumentVersion};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{
|
app_state::{
|
||||||
AppState,
|
AppState,
|
||||||
broadcasts::VaultUpdate,
|
|
||||||
database::models::{
|
database::models::{
|
||||||
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
|
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
|
||||||
},
|
},
|
||||||
|
|
@ -22,8 +19,7 @@ use crate::{
|
||||||
utils::{normalize::normalize, sanitize_path::sanitize_path},
|
utils::{normalize::normalize, sanitize_path::sanitize_path},
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct DeleteDocumentPathParams {
|
pub struct DeleteDocumentPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
@ -38,7 +34,7 @@ pub async fn delete_document(
|
||||||
document_id,
|
document_id,
|
||||||
}): Path<DeleteDocumentPathParams>,
|
}): Path<DeleteDocumentPathParams>,
|
||||||
Extension(user): Extension<User>,
|
Extension(user): Extension<User>,
|
||||||
TypedHeader(user_agent): TypedHeader<DeviceIdHeader>,
|
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(request): Json<DeleteDocumentVersion>,
|
Json(request): Json<DeleteDocumentVersion>,
|
||||||
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
|
||||||
|
|
@ -69,7 +65,7 @@ pub async fn delete_document(
|
||||||
updated_date: chrono::Utc::now(),
|
updated_date: chrono::Utc::now(),
|
||||||
is_deleted: true,
|
is_deleted: true,
|
||||||
user_id: user.name,
|
user_id: user.name,
|
||||||
device_id: user_agent.0,
|
device_id: device_id.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|
@ -84,16 +80,5 @@ pub async fn delete_document(
|
||||||
.context("Failed to commit successful transaction")
|
.context("Failed to commit successful transaction")
|
||||||
.map_err(server_error)?;
|
.map_err(server_error)?;
|
||||||
|
|
||||||
state
|
|
||||||
.broadcasts
|
|
||||||
.send(
|
|
||||||
vault_id,
|
|
||||||
VaultUpdate {
|
|
||||||
origin_device_id: request.device_id,
|
|
||||||
document: new_version.clone().into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(Json(new_version.into()))
|
Ok(Json(new_version.into()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::extract::{Path, State};
|
use axum::{
|
||||||
use axum_jsonschema::Json;
|
Json,
|
||||||
use schemars::JsonSchema;
|
extract::{Path, State},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -13,8 +14,7 @@ use crate::{
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct FetchDocumentVersionPathParams {
|
pub struct FetchDocumentVersionPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -15,8 +14,7 @@ use crate::{
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct FetchDocumentVersionContentPathParams {
|
pub struct FetchDocumentVersionContentPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::extract::{Path, State};
|
use axum::{
|
||||||
use axum_jsonschema::Json;
|
Json,
|
||||||
use schemars::JsonSchema;
|
extract::{Path, State},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -13,8 +14,7 @@ use crate::{
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct FetchLatestDocumentVersionPathParams {
|
pub struct FetchLatestDocumentVersionPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::{
|
||||||
use axum_jsonschema::Json;
|
Json,
|
||||||
use schemars::JsonSchema;
|
extract::{Path, Query, State},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::responses::FetchLatestDocumentsResponse;
|
use super::responses::FetchLatestDocumentsResponse;
|
||||||
|
|
@ -13,15 +14,13 @@ use crate::{
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct FetchLatestDocumentsPathParams {
|
pub struct FetchLatestDocumentsPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct QueryParams {
|
pub struct QueryParams {
|
||||||
since_update_id: Option<VaultUpdateId>,
|
since_update_id: Option<VaultUpdateId>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
backend/sync_server/src/server/index.rs
Normal file
7
backend/sync_server/src/server/index.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use axum::response::{Html, IntoResponse};
|
||||||
|
|
||||||
|
pub async fn index() -> impl IntoResponse {
|
||||||
|
const HTML_CONTENT: &str = include_str!("./assets/index.html");
|
||||||
|
let html_content = HTML_CONTENT;
|
||||||
|
Html(html_content)
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ use axum_extra::{
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
headers::{Authorization, authorization::Bearer},
|
headers::{Authorization, authorization::Bearer},
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{auth::auth, responses::PingResponse};
|
use super::{auth::auth, responses::PingResponse};
|
||||||
|
|
@ -16,8 +15,7 @@ use crate::{
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct PingPathParams {
|
pub struct PingPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use aide_axum_typed_multipart::FieldData;
|
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use axum_typed_multipart::TryFromMultipart;
|
use axum_typed_multipart::{FieldData, TryFromMultipart};
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{self, Deserialize};
|
use serde::{self, Deserialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::app_state::database::models::{DeviceId, DocumentId, VaultUpdateId};
|
use crate::app_state::database::models::{DocumentId, VaultUpdateId};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(TS, Debug, TryFromMultipart)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[ts(export)]
|
||||||
pub struct CreateDocumentVersion {
|
pub struct CreateDocumentVersion {
|
||||||
/// The client can decide the document id (if it wishes to) in order
|
/// The client can decide the document id (if it wishes to) in order
|
||||||
/// to help with syncing. If the client does not provide a document id,
|
/// to help with syncing. If the client does not provide a document id,
|
||||||
|
|
@ -15,41 +14,26 @@ pub struct CreateDocumentVersion {
|
||||||
/// it must not already exist in the database.
|
/// it must not already exist in the database.
|
||||||
pub document_id: Option<DocumentId>,
|
pub document_id: Option<DocumentId>,
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub content_base64: String,
|
|
||||||
pub device_id: Option<DeviceId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, TryFromMultipart, JsonSchema)]
|
#[ts(as = "Vec<u8>")]
|
||||||
pub struct CreateDocumentVersionMultipart {
|
|
||||||
pub document_id: Option<DocumentId>,
|
|
||||||
pub relative_path: String,
|
|
||||||
#[form_data(limit = "unlimited")]
|
#[form_data(limit = "unlimited")]
|
||||||
pub content: FieldData<Bytes>,
|
pub content: FieldData<Bytes>,
|
||||||
pub device_id: Option<DeviceId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(TS, Debug, TryFromMultipart)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[ts(export)]
|
||||||
pub struct UpdateDocumentVersion {
|
pub struct UpdateDocumentVersion {
|
||||||
pub parent_version_id: VaultUpdateId,
|
pub parent_version_id: VaultUpdateId,
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub content_base64: String,
|
|
||||||
pub device_id: Option<DeviceId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, TryFromMultipart, JsonSchema)]
|
#[ts(as = "Vec<u8>")]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UpdateDocumentVersionMultipart {
|
|
||||||
pub parent_version_id: VaultUpdateId,
|
|
||||||
pub relative_path: String,
|
|
||||||
#[form_data(limit = "unlimited")]
|
#[form_data(limit = "unlimited")]
|
||||||
pub content: FieldData<Bytes>,
|
pub content: FieldData<Bytes>,
|
||||||
pub device_id: Option<DeviceId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(TS, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct DeleteDocumentVersion {
|
pub struct DeleteDocumentVersion {
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub device_id: Option<DeviceId>,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{self, Serialize};
|
use serde::{self, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::app_state::database::models::{
|
use crate::app_state::database::models::{
|
||||||
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
|
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Response to a ping request.
|
/// Response to a ping request.
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct PingResponse {
|
pub struct PingResponse {
|
||||||
/// Semantic version of the server.
|
/// Semantic version of the server.
|
||||||
pub server_version: String,
|
pub server_version: String,
|
||||||
|
|
@ -18,8 +19,9 @@ pub struct PingResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a fetch latest documents request.
|
/// Response to a fetch latest documents request.
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct FetchLatestDocumentsResponse {
|
pub struct FetchLatestDocumentsResponse {
|
||||||
pub latest_documents: Vec<DocumentVersionWithoutContent>,
|
pub latest_documents: Vec<DocumentVersionWithoutContent>,
|
||||||
|
|
||||||
|
|
@ -28,8 +30,9 @@ pub struct FetchLatestDocumentsResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to an update document request.
|
/// Response to an update document request.
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
|
#[ts(export)]
|
||||||
pub enum DocumentUpdateResponse {
|
pub enum DocumentUpdateResponse {
|
||||||
/// Returned when the created/updated document's content is the same as was
|
/// Returned when the created/updated document's content is the same as was
|
||||||
/// sent in the create/update request and thus the response doesn't contain
|
/// sent in the create/update request and thus the response doesn't contain
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,29 @@
|
||||||
use aide_axum_typed_multipart::TypedMultipart;
|
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension, Json,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
};
|
};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use axum_jsonschema::Json;
|
use axum_typed_multipart::TypedMultipart;
|
||||||
use log::info;
|
use log::info;
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sync_lib::{base64_to_bytes, is_file_type_mergable, merge};
|
use sync_lib::{is_file_type_mergable, merge};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
device_id_header::DeviceIdHeader,
|
device_id_header::DeviceIdHeader, requests::UpdateDocumentVersion,
|
||||||
requests::{UpdateDocumentVersion, UpdateDocumentVersionMultipart},
|
|
||||||
responses::DocumentUpdateResponse,
|
responses::DocumentUpdateResponse,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{
|
app_state::{
|
||||||
AppState,
|
AppState,
|
||||||
broadcasts::VaultUpdate,
|
database::models::{DocumentId, StoredDocumentVersion, VaultId},
|
||||||
database::models::{DeviceId, DocumentId, StoredDocumentVersion, VaultId, VaultUpdateId},
|
|
||||||
},
|
},
|
||||||
config::user_config::User,
|
config::user_config::User,
|
||||||
errors::{SyncServerError, client_error, not_found_error, server_error},
|
errors::{SyncServerError, not_found_error, server_error},
|
||||||
utils::{dedup_paths::dedup_paths, normalize::normalize, sanitize_path::sanitize_path},
|
utils::{dedup_paths::dedup_paths, normalize::normalize, sanitize_path::sanitize_path},
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
|
||||||
pub struct UpdateDocumentPathParams {
|
pub struct UpdateDocumentPathParams {
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
|
|
@ -37,90 +32,34 @@ pub struct UpdateDocumentPathParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn update_document_multipart(
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub async fn update_document(
|
||||||
Path(UpdateDocumentPathParams {
|
Path(UpdateDocumentPathParams {
|
||||||
vault_id,
|
vault_id,
|
||||||
document_id,
|
document_id,
|
||||||
}): Path<UpdateDocumentPathParams>,
|
}): Path<UpdateDocumentPathParams>,
|
||||||
Extension(user): Extension<User>,
|
Extension(user): Extension<User>,
|
||||||
TypedHeader(user_agent): TypedHeader<DeviceIdHeader>,
|
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart<
|
TypedMultipart(request): TypedMultipart<UpdateDocumentVersion>,
|
||||||
UpdateDocumentVersionMultipart,
|
|
||||||
>,
|
|
||||||
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
|
|
||||||
internal_update_document(
|
|
||||||
user,
|
|
||||||
user_agent,
|
|
||||||
state,
|
|
||||||
vault_id,
|
|
||||||
document_id,
|
|
||||||
request.parent_version_id,
|
|
||||||
request.relative_path,
|
|
||||||
request.device_id,
|
|
||||||
request.content.contents.to_vec(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[axum::debug_handler]
|
|
||||||
pub async fn update_document_json(
|
|
||||||
Path(UpdateDocumentPathParams {
|
|
||||||
vault_id,
|
|
||||||
document_id,
|
|
||||||
}): Path<UpdateDocumentPathParams>,
|
|
||||||
Extension(user): Extension<User>,
|
|
||||||
TypedHeader(user_agent): TypedHeader<DeviceIdHeader>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Json(request): Json<UpdateDocumentVersion>,
|
|
||||||
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
|
|
||||||
let content_bytes = base64_to_bytes(&request.content_base64)
|
|
||||||
.context("Failed to decode base64 content in request")
|
|
||||||
.map_err(client_error)?;
|
|
||||||
|
|
||||||
internal_update_document(
|
|
||||||
user,
|
|
||||||
user_agent,
|
|
||||||
state,
|
|
||||||
vault_id,
|
|
||||||
document_id,
|
|
||||||
request.parent_version_id,
|
|
||||||
request.relative_path,
|
|
||||||
request.device_id,
|
|
||||||
content_bytes,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
|
||||||
async fn internal_update_document(
|
|
||||||
user: User,
|
|
||||||
user_agent: DeviceIdHeader,
|
|
||||||
state: AppState,
|
|
||||||
vault_id: VaultId,
|
|
||||||
document_id: DocumentId,
|
|
||||||
parent_version_id: VaultUpdateId,
|
|
||||||
relative_path: String,
|
|
||||||
device_id: Option<DeviceId>,
|
|
||||||
content: Vec<u8>,
|
|
||||||
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
|
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
|
||||||
// No need for a transaction as document versions are immutable
|
// No need for a transaction as document versions are immutable
|
||||||
let parent_document = state
|
let parent_document = state
|
||||||
.database
|
.database
|
||||||
.get_document_version(&vault_id, parent_version_id, None)
|
.get_document_version(&vault_id, request.parent_version_id, None)
|
||||||
.await
|
.await
|
||||||
.map_err(server_error)?
|
.map_err(server_error)?
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| {
|
|| {
|
||||||
Err(not_found_error(anyhow!(
|
Err(not_found_error(anyhow!(
|
||||||
"Parent version with id `{}` not found",
|
"Parent version with id `{}` not found",
|
||||||
parent_version_id
|
request.parent_version_id
|
||||||
)))
|
)))
|
||||||
},
|
},
|
||||||
Ok,
|
Ok,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let sanitized_relative_path = sanitize_path(&relative_path);
|
let sanitized_relative_path = sanitize_path(&request.relative_path);
|
||||||
|
|
||||||
let mut transaction = state
|
let mut transaction = state
|
||||||
.database
|
.database
|
||||||
|
|
@ -160,6 +99,8 @@ async fn internal_update_document(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let content = request.content.contents.to_vec();
|
||||||
|
|
||||||
// Return the latest version if the content and path are the same as the latest
|
// Return the latest version if the content and path are the same as the latest
|
||||||
// version
|
// version
|
||||||
if content == latest_version.content && sanitized_relative_path == latest_version.relative_path
|
if content == latest_version.content && sanitized_relative_path == latest_version.relative_path
|
||||||
|
|
@ -215,7 +156,7 @@ async fn internal_update_document(
|
||||||
updated_date: chrono::Utc::now(),
|
updated_date: chrono::Utc::now(),
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
user_id: user.name,
|
user_id: user.name,
|
||||||
device_id: user_agent.0,
|
device_id: device_id.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|
@ -230,17 +171,6 @@ async fn internal_update_document(
|
||||||
.context("Failed to commit successful transaction")
|
.context("Failed to commit successful transaction")
|
||||||
.map_err(server_error)?;
|
.map_err(server_error)?;
|
||||||
|
|
||||||
state
|
|
||||||
.broadcasts
|
|
||||||
.send(
|
|
||||||
vault_id,
|
|
||||||
VaultUpdate {
|
|
||||||
origin_device_id: device_id,
|
|
||||||
document: new_version.clone().into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(Json(if is_different_from_request_content {
|
Ok(Json(if is_different_from_request_content {
|
||||||
DocumentUpdateResponse::MergingUpdate(new_version.into())
|
DocumentUpdateResponse::MergingUpdate(new_version.into())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -6,165 +6,176 @@ use axum::{
|
||||||
},
|
},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::stream::StreamExt;
|
||||||
sink::SinkExt,
|
use log::{debug, info};
|
||||||
stream::{SplitSink, StreamExt},
|
use serde::Deserialize;
|
||||||
};
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::auth::auth;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{
|
app_state::{
|
||||||
AppState,
|
AppState,
|
||||||
database::models::{DeviceId, DocumentVersionWithoutContent, VaultId, VaultUpdateId},
|
database::models::VaultId,
|
||||||
|
websocket::{
|
||||||
|
models::{
|
||||||
|
CursorPositionFromServer, WebSocketClientMessage, WebSocketServerMessage,
|
||||||
|
WebSocketVaultUpdate,
|
||||||
|
},
|
||||||
|
utils::{
|
||||||
|
get_authenticated_handshake, get_unseen_documents, send_update_over_websocket,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
errors::{SyncServerError, server_error, unauthenticated_error},
|
errors::{SyncServerError, client_error, server_error},
|
||||||
utils::normalize::normalize,
|
utils::normalize::normalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is required for aide to infer the path parameter types and names
|
#[derive(Deserialize)]
|
||||||
#[derive(Deserialize, JsonSchema)]
|
pub struct WebSocketPathParams {
|
||||||
pub struct WebsocketPathParams {
|
|
||||||
#[serde(deserialize_with = "normalize")]
|
#[serde(deserialize_with = "normalize")]
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn websocket_handler(
|
pub async fn websocket_handler(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
Path(WebsocketPathParams { vault_id }): Path<WebsocketPathParams>,
|
Path(WebSocketPathParams { vault_id }): Path<WebSocketPathParams>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Response, SyncServerError> {
|
) -> Result<Response, SyncServerError> {
|
||||||
Ok(ws.on_upgrade(move |socket| websocket_wrapped(state, socket, vault_id)))
|
Ok(ws.on_upgrade(move |socket| websocket_wrapped(state, socket, vault_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn websocket_wrapped(state: AppState, stream: WebSocket, vault_id: VaultId) {
|
async fn websocket_wrapped(state: AppState, stream: WebSocket, vault_id: VaultId) {
|
||||||
info!("Websocket connection opened on vault '{vault_id}'");
|
info!("WebSocket connection opened on vault '{vault_id}'");
|
||||||
|
|
||||||
let result = websocket(state, stream, vault_id.clone()).await;
|
let result = websocket(state, stream, vault_id.clone()).await;
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
error!("Websocket connection error on vault '{vault_id}': {err}");
|
debug!("WebSocket connection error on vault '{vault_id}': {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!("Websocket connection closed on vault '{vault_id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct WebsocketHandshake {
|
|
||||||
pub token: String,
|
|
||||||
pub device_id: DeviceId,
|
|
||||||
pub last_seen_vault_update_id: Option<VaultUpdateId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct WebsocketVaultUpdate {
|
|
||||||
pub documents: Vec<DocumentVersionWithoutContent>,
|
|
||||||
pub is_initial_sync: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn websocket(
|
async fn websocket(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
stream: WebSocket,
|
stream: WebSocket,
|
||||||
vault_id: VaultId,
|
vault_id: VaultId,
|
||||||
) -> Result<(), SyncServerError> {
|
) -> Result<(), SyncServerError> {
|
||||||
let (mut sender, mut receiver) = stream.split();
|
let (mut sender, mut websocket_receiver) = stream.split();
|
||||||
|
|
||||||
let handshake = if let Some(Ok(Message::Text(token))) = receiver.next().await {
|
let authed_handshake = get_authenticated_handshake(
|
||||||
let handshake: WebsocketHandshake = serde_json::from_str(&token)
|
&state,
|
||||||
.context("Failed to parse token")
|
&vault_id,
|
||||||
.map_err(server_error)?;
|
websocket_receiver
|
||||||
|
.next()
|
||||||
auth(&state, handshake.token.trim(), &vault_id)?;
|
|
||||||
|
|
||||||
handshake
|
|
||||||
} else {
|
|
||||||
return Err(unauthenticated_error(anyhow::anyhow!(
|
|
||||||
"Failed to authenticate"
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rx = state.broadcasts.get_receiver(vault_id.clone()).await;
|
|
||||||
|
|
||||||
let documents = if let Some(update_id) = handshake.last_seen_vault_update_id {
|
|
||||||
state
|
|
||||||
.database
|
|
||||||
.get_latest_documents_since(&vault_id, update_id, None)
|
|
||||||
.await
|
.await
|
||||||
.map_err(server_error)
|
.transpose()
|
||||||
} else {
|
.unwrap_or_default(),
|
||||||
state
|
)?;
|
||||||
.database
|
|
||||||
.get_latest_documents(&vault_id, None)
|
info!(
|
||||||
.await
|
"WebSocket handshake successful for vault '{vault_id}' for '{}'",
|
||||||
.map_err(server_error)
|
authed_handshake.handshake.device_id
|
||||||
}?;
|
);
|
||||||
|
|
||||||
|
let mut broadcast_receiver = state.broadcasts.get_receiver(vault_id.clone()).await;
|
||||||
|
|
||||||
send_update_over_websocket(
|
send_update_over_websocket(
|
||||||
&WebsocketVaultUpdate {
|
&WebSocketServerMessage::VaultUpdate(WebSocketVaultUpdate {
|
||||||
documents,
|
documents: get_unseen_documents(
|
||||||
|
&state,
|
||||||
|
&vault_id,
|
||||||
|
authed_handshake.handshake.last_seen_vault_update_id,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
is_initial_sync: true,
|
is_initial_sync: true,
|
||||||
},
|
}),
|
||||||
&mut sender,
|
&mut sender,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
send_update_over_websocket(
|
||||||
|
&WebSocketServerMessage::CursorPositions(CursorPositionFromServer {
|
||||||
|
clients: state.cursors.get_cursors(&vault_id).await,
|
||||||
|
}),
|
||||||
|
&mut sender,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let device_id = authed_handshake.handshake.device_id.clone();
|
||||||
let mut send_task = tokio::spawn(async move {
|
let mut send_task = tokio::spawn(async move {
|
||||||
while let Ok(update) = rx.recv().await {
|
while let Ok(update) = broadcast_receiver.recv().await {
|
||||||
if Some(&handshake.device_id) == update.origin_device_id.as_ref() {
|
if Some(&device_id) == update.origin_device_id.as_ref() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_update_over_websocket(
|
send_update_over_websocket(&update.message, &mut sender).await?;
|
||||||
&WebsocketVaultUpdate {
|
|
||||||
documents: vec![update.document],
|
|
||||||
is_initial_sync: false,
|
|
||||||
},
|
|
||||||
&mut sender,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), SyncServerError>(())
|
Ok::<(), SyncServerError>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut recv_task =
|
let device_id = authed_handshake.handshake.device_id.clone();
|
||||||
tokio::spawn(
|
let vault_id_clone = vault_id.clone();
|
||||||
async move { while let Some(Ok(Message::Text(_text))) = receiver.next().await {} },
|
let cursor_manager = state.cursors.clone();
|
||||||
);
|
let mut receive_task = tokio::spawn(async move {
|
||||||
|
while let Some(Ok(Message::Text(message))) = websocket_receiver.next().await {
|
||||||
|
let message: WebSocketClientMessage = serde_json::from_str(&message)
|
||||||
|
.context("Failed to parse WebSocket message from client")
|
||||||
|
.map_err(server_error)?;
|
||||||
|
|
||||||
|
match message {
|
||||||
|
WebSocketClientMessage::Handshake(_) => {
|
||||||
|
return Err(client_error(anyhow::anyhow!(
|
||||||
|
"Unexpected handshake message"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
WebSocketClientMessage::CursorPositions(cursors) => {
|
||||||
|
cursor_manager
|
||||||
|
.update_cursors(
|
||||||
|
vault_id_clone.clone(),
|
||||||
|
authed_handshake.user.name.clone(),
|
||||||
|
&device_id,
|
||||||
|
cursors.document_to_cursors,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), SyncServerError>(())
|
||||||
|
});
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = &mut send_task => recv_task.abort(),
|
_ = &mut send_task => receive_task.abort(),
|
||||||
_ = &mut recv_task => send_task.abort(),
|
_ = &mut receive_task => send_task.abort(),
|
||||||
};
|
};
|
||||||
|
|
||||||
send_task
|
let result: Result<(), SyncServerError> = (async {
|
||||||
.await
|
send_task
|
||||||
.context("Websocket send task failed")
|
.await
|
||||||
.map_err(server_error)??;
|
.context("WebSocket send task failed")
|
||||||
|
.map_err(client_error)
|
||||||
|
.and_then(|err| err)?;
|
||||||
|
|
||||||
recv_task
|
receive_task
|
||||||
.await
|
.await
|
||||||
.context("Websocket receive task failed")
|
.context("WebSocket receive task failed")
|
||||||
.map_err(server_error)?;
|
.map_err(client_error)
|
||||||
|
.and_then(|err| err)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
})
|
||||||
|
.await;
|
||||||
async fn send_update_over_websocket(
|
|
||||||
update: &WebsocketVaultUpdate,
|
state
|
||||||
sender: &mut SplitSink<WebSocket, Message>,
|
.cursors
|
||||||
) -> Result<(), SyncServerError> {
|
.remove_cursors_of_device(&vault_id, &authed_handshake.handshake.device_id)
|
||||||
let serialized_update = serde_json::to_string(update)
|
.await;
|
||||||
.context("Failed to serialize update")
|
|
||||||
.map_err(server_error)?;
|
if result.is_err() {
|
||||||
|
info!(
|
||||||
sender
|
"WebSocket disconnected on vault '{vault_id}' for '{}'",
|
||||||
.send(Message::Text(serialized_update))
|
authed_handshake.handshake.device_id
|
||||||
.await
|
);
|
||||||
.context("Failed to send message over websocket")
|
}
|
||||||
.map_err(server_error)
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,39 @@
|
||||||
{
|
{
|
||||||
"name": "vault-link-obsidian-plugin",
|
"name": "vault-link-obsidian-plugin",
|
||||||
"version": "0.3.15",
|
"version": "0.3.15",
|
||||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack watch --mode development",
|
"dev": "webpack watch --mode development",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"version": "node version-bump.mjs"
|
"version": "node version-bump.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"mini-css-extract-plugin": "^2.9.2",
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
"obsidian": "1.8.7",
|
"obsidian": "1.8.7",
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.1",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"sync-client": "file:../sync-client",
|
"sync-client": "file:../sync-client",
|
||||||
"terser-webpack-plugin": "^5.3.14",
|
"terser-webpack-plugin": "^5.3.14",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"url": "^0.11.4",
|
"url": "^0.11.4",
|
||||||
"virtual-scroller": "^1.13.1",
|
"virtual-scroller": "^1.13.1",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
} from "sync-client";
|
} from "sync-client";
|
||||||
import { lineAndColumnToPosition } from "./utils/line-and-column-to-position";
|
import { lineAndColumnToPosition } from "./utils/line-and-column-to-position";
|
||||||
import { positionToLineAndColumn } from "./utils/position-to-line-and-column";
|
import { positionToLineAndColumn } from "./utils/position-to-line-and-column";
|
||||||
|
import { getCursorsFromEditor } from "./views/cursors/get-cursors-from-editor";
|
||||||
|
|
||||||
export class ObsidianFileSystemOperations implements FileSystemOperations {
|
export class ObsidianFileSystemOperations implements FileSystemOperations {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
|
@ -78,26 +79,19 @@ export class ObsidianFileSystemOperations implements FileSystemOperations {
|
||||||
|
|
||||||
if (view?.file?.path === path) {
|
if (view?.file?.path === path) {
|
||||||
const text = view.editor.getValue();
|
const text = view.editor.getValue();
|
||||||
const cursors = view.editor
|
|
||||||
.listSelections()
|
const cursors = getCursorsFromEditor(view.editor).flatMap(
|
||||||
.flatMap(({ anchor, head }, i) => [
|
({ id, start: anchor, end: head }) => [
|
||||||
{
|
{
|
||||||
id: 2 * i,
|
id: 2 * id,
|
||||||
characterPosition: lineAndColumnToPosition(
|
characterPosition: anchor
|
||||||
text,
|
|
||||||
anchor.line,
|
|
||||||
anchor.ch
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2 * i + 1,
|
id: 2 * id + 1,
|
||||||
characterPosition: lineAndColumnToPosition(
|
characterPosition: head
|
||||||
text,
|
|
||||||
head.line,
|
|
||||||
head.ch
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const result = updater({
|
const result = updater({
|
||||||
text,
|
text,
|
||||||
|
|
|
||||||
9
frontend/obsidian-plugin/src/utils/get-random-color.ts
Normal file
9
frontend/obsidian-plugin/src/utils/get-random-color.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function getRandomColor(name: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = (hash << 5) - hash + name.charCodeAt(i);
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
const normalised = hash / 0x7fffffff;
|
||||||
|
return `hsl(${Math.abs(normalised * 360)}, 55%, 55%)`; // HSL color
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,36 @@
|
||||||
import type {
|
import type {
|
||||||
Editor,
|
Editor,
|
||||||
|
EventRef,
|
||||||
MarkdownFileInfo,
|
MarkdownFileInfo,
|
||||||
MarkdownView,
|
|
||||||
TAbstractFile,
|
TAbstractFile,
|
||||||
|
Workspace,
|
||||||
WorkspaceLeaf
|
WorkspaceLeaf
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
import type { MarkdownView } from "obsidian";
|
||||||
import { Platform, Plugin, TFile } from "obsidian";
|
import { Platform, Plugin, TFile } from "obsidian";
|
||||||
import "../manifest.json";
|
import "../manifest.json";
|
||||||
import { HistoryView } from "./views/history/history-view";
|
import { HistoryView } from "./views/history/history-view";
|
||||||
import { StatusBar } from "./views/status-bar/status-bar";
|
import { StatusBar } from "./views/status-bar/status-bar";
|
||||||
import { LogsView } from "./views/logs/logs-view";
|
import { LogsView } from "./views/logs/logs-view";
|
||||||
import { StatusDescription } from "./views/status-description/status-description";
|
import { StatusDescription } from "./views/status-description/status-description";
|
||||||
|
import type { CursorSpan, RelativePath } from "sync-client";
|
||||||
import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client";
|
import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client";
|
||||||
import { ObsidianFileSystemOperations } from "./obsidian-file-system";
|
import { ObsidianFileSystemOperations } from "./obsidian-file-system";
|
||||||
import { SyncSettingsTab } from "./views/settings/settings-tab";
|
import { SyncSettingsTab } from "./views/settings/settings-tab";
|
||||||
import { registerConsoleForLogging } from "./utils/register-console-for-logging";
|
import { registerConsoleForLogging } from "./utils/register-console-for-logging";
|
||||||
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
|
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
|
||||||
|
import { remoteCursorsTheme } from "./views/cursors/remote-cursor-theme";
|
||||||
|
import {
|
||||||
|
remoteCursorsPlugin,
|
||||||
|
setCursors
|
||||||
|
} from "./views/cursors/remote-cursors-plugin";
|
||||||
|
import { getCursorsFromEditor } from "./views/cursors/get-cursors-from-editor";
|
||||||
|
import { LocalCursorUpdateListener } from "./views/cursors/local-cursor-update-listener";
|
||||||
|
|
||||||
|
const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250;
|
||||||
export default class VaultLinkPlugin extends Plugin {
|
export default class VaultLinkPlugin extends Plugin {
|
||||||
private readonly disposables: (() => void)[] = [];
|
private readonly disposables: (() => unknown)[] = [];
|
||||||
|
|
||||||
private settingsTab: SyncSettingsTab | undefined;
|
private settingsTab: SyncSettingsTab | undefined;
|
||||||
private client!: SyncClient;
|
private client!: SyncClient;
|
||||||
private readonly rateLimitedUpdatesPerFile = new Map<
|
private readonly rateLimitedUpdatesPerFile = new Map<
|
||||||
|
|
@ -61,18 +73,36 @@ export default class VaultLinkPlugin extends Plugin {
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(
|
||||||
HistoryView.TYPE,
|
HistoryView.TYPE,
|
||||||
(leaf) => new HistoryView(leaf, this.client)
|
(leaf) => new HistoryView(this.client, leaf)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(
|
||||||
LogsView.TYPE,
|
LogsView.TYPE,
|
||||||
(leaf) => new LogsView(this.client, leaf)
|
(leaf) => new LogsView(this.client, leaf)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
|
||||||
|
|
||||||
|
this.client.addRemoteCursorsUpdateListener((cursors) => {
|
||||||
|
setCursors(cursors, this.app);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cursorListener = new LocalCursorUpdateListener(
|
||||||
|
this.client,
|
||||||
|
this.app.workspace
|
||||||
|
);
|
||||||
|
this.disposables.push(() => {
|
||||||
|
cursorListener.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.workspace.updateOptions();
|
||||||
|
|
||||||
this.addRibbonIcon(
|
this.addRibbonIcon(
|
||||||
HistoryView.ICON,
|
HistoryView.ICON,
|
||||||
"Open VaultLink events",
|
"Open VaultLink events",
|
||||||
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addRibbonIcon(
|
this.addRibbonIcon(
|
||||||
LogsView.ICON,
|
LogsView.ICON,
|
||||||
"Open VaultLink logs",
|
"Open VaultLink logs",
|
||||||
|
|
@ -181,7 +211,7 @@ export default class VaultLinkPlugin extends Plugin {
|
||||||
this.client.syncLocallyUpdatedFile({
|
this.client.syncLocallyUpdatedFile({
|
||||||
relativePath: path
|
relativePath: path
|
||||||
}),
|
}),
|
||||||
250
|
MIN_WAIT_BETWEEN_UPDATES_IN_MS
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Editor } from "obsidian";
|
||||||
|
import { lineAndColumnToPosition } from "../../utils/line-and-column-to-position";
|
||||||
|
|
||||||
|
export interface Cursor {
|
||||||
|
id: number;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCursorsFromEditor(editor: Editor): Cursor[] {
|
||||||
|
const text = editor.getValue();
|
||||||
|
return editor.listSelections().map(({ anchor, head }, i) => ({
|
||||||
|
id: i,
|
||||||
|
start: lineAndColumnToPosition(text, anchor.line, anchor.ch),
|
||||||
|
end: lineAndColumnToPosition(text, head.line, head.ch)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import type { Workspace } from "obsidian";
|
||||||
|
import { EventRef, Editor, MarkdownView, MarkdownFileInfo } from "obsidian";
|
||||||
|
import type { Logger, SyncClient } from "sync-client";
|
||||||
|
import type { Cursor } from "./get-cursors-from-editor";
|
||||||
|
import { getCursorsFromEditor } from "./get-cursors-from-editor";
|
||||||
|
|
||||||
|
export class LocalCursorUpdateListener {
|
||||||
|
private static readonly UPDATE_INTERVAL_MS = 50;
|
||||||
|
private readonly eventHandle: NodeJS.Timeout;
|
||||||
|
private lastCursorState: Record<string, Cursor[]> = {};
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly client: SyncClient,
|
||||||
|
private readonly workspace: Workspace
|
||||||
|
) {
|
||||||
|
this.eventHandle = setInterval(() => {
|
||||||
|
this.updateAllCursors();
|
||||||
|
}, LocalCursorUpdateListener.UPDATE_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
clearInterval(this.eventHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAllCursors(): void {
|
||||||
|
const currentCursors = this.getAllCursors();
|
||||||
|
if (
|
||||||
|
JSON.stringify(this.lastCursorState) ===
|
||||||
|
JSON.stringify(currentCursors)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastCursorState = currentCursors;
|
||||||
|
this.client
|
||||||
|
.updateLocalCursors(currentCursors)
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
this.client.logger.error(
|
||||||
|
`Failed to update local cursors: ${error}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllCursors(): Record<string, Cursor[]> {
|
||||||
|
const cursors: Record<string, Cursor[]> = {};
|
||||||
|
this.workspace
|
||||||
|
.getLeavesOfType("markdown")
|
||||||
|
.map((leaf) => leaf.view)
|
||||||
|
.filter((view) => view instanceof MarkdownView)
|
||||||
|
.forEach((view) => {
|
||||||
|
const { file } = view;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cursors[file.path] = getCursorsFromEditor(view.editor);
|
||||||
|
});
|
||||||
|
return cursors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
const CARET_WIDTH = 2;
|
||||||
|
const DOT_RADIUS = 4;
|
||||||
|
|
||||||
|
export const remoteCursorsTheme = EditorView.baseTheme({
|
||||||
|
".selection-caret": {
|
||||||
|
position: "relative"
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret > *": {
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: "inherit"
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret > .stick": {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
width: `${CARET_WIDTH}px`,
|
||||||
|
height: "100%",
|
||||||
|
display: "block",
|
||||||
|
borderRadius: `${CARET_WIDTH / 2}px`,
|
||||||
|
animation: "blink-stick 1s steps(1) infinite"
|
||||||
|
},
|
||||||
|
|
||||||
|
"@keyframes blink-stick": {
|
||||||
|
"0%, 100%": { opacity: 1 },
|
||||||
|
"50%": { opacity: 0 }
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret > .dot": {
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: `${DOT_RADIUS * 2}px`,
|
||||||
|
height: `${DOT_RADIUS * 2}px`,
|
||||||
|
top: `-${DOT_RADIUS}px`,
|
||||||
|
left: `-${DOT_RADIUS}px`,
|
||||||
|
transition: "transform .3s ease-in-out",
|
||||||
|
transformOrigin: "bottom center",
|
||||||
|
boxSizing: "border-box"
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret:hover > .dot": {
|
||||||
|
transform: "scale(0)"
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret > .info": {
|
||||||
|
top: "-1.3em",
|
||||||
|
left: `-${CARET_WIDTH / 2}px`,
|
||||||
|
fontSize: "0.9em",
|
||||||
|
userSelect: "none",
|
||||||
|
color: "white",
|
||||||
|
padding: "0 2px",
|
||||||
|
transition: "opacity .3s ease-in-out",
|
||||||
|
opacity: 0,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
borderRadius: "3px 3px 3px 0"
|
||||||
|
},
|
||||||
|
|
||||||
|
".selection-caret:hover > .info": {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { AnnotationType, Annotation, RangeSet, Range } from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
Decoration,
|
||||||
|
WidgetType
|
||||||
|
} from "@codemirror/view";
|
||||||
|
|
||||||
|
import type { PluginValue, DecorationSet, EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
export class RemoteCursorWidget extends WidgetType {
|
||||||
|
public constructor(
|
||||||
|
private readonly color: string,
|
||||||
|
private readonly name: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toDOM(editor: EditorView): HTMLElement {
|
||||||
|
return editor.contentDOM.createEl(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
cls: "selection-caret",
|
||||||
|
attr: {
|
||||||
|
style: `background-color: ${this.color}; border-color: ${this.color}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(span) => {
|
||||||
|
span.createEl("div", {
|
||||||
|
cls: "stick"
|
||||||
|
});
|
||||||
|
span.createEl("div", {
|
||||||
|
cls: "dot"
|
||||||
|
});
|
||||||
|
span.createEl("div", {
|
||||||
|
cls: "info",
|
||||||
|
text: this.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public eq(other: RemoteCursorWidget): boolean {
|
||||||
|
return other.color === this.color && other.name === this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import type { Range } from "@codemirror/state";
|
||||||
|
import { RangeSet, Annotation, AnnotationType } from "@codemirror/state";
|
||||||
|
import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
PluginValue,
|
||||||
|
DecorationSet,
|
||||||
|
EditorView,
|
||||||
|
ViewUpdate
|
||||||
|
} from "@codemirror/view";
|
||||||
|
import { RemoteCursorWidget } from "./remote-cursor-widget";
|
||||||
|
import type { ClientCursors, CursorSpan } from "sync-client";
|
||||||
|
import type { App } from "obsidian";
|
||||||
|
import { MarkdownView } from "obsidian";
|
||||||
|
|
||||||
|
let cursors: {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
span: CursorSpan;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
import { StateEffect } from "@codemirror/state";
|
||||||
|
import { getRandomColor } from "src/utils/get-random-color";
|
||||||
|
|
||||||
|
const forceUpdate = StateEffect.define();
|
||||||
|
|
||||||
|
export class RemoteCursorsPluginValue implements PluginValue {
|
||||||
|
public decorations: DecorationSet = RangeSet.of([]);
|
||||||
|
|
||||||
|
public update(update: ViewUpdate): void {
|
||||||
|
const decorations: Range<Decoration>[] = [];
|
||||||
|
|
||||||
|
cursors.forEach(({ name, span: { start, end } }) => {
|
||||||
|
const color = getRandomColor(name);
|
||||||
|
const startLine = update.view.state.doc.lineAt(start);
|
||||||
|
const endLine = update.view.state.doc.lineAt(end);
|
||||||
|
|
||||||
|
const attributes = {
|
||||||
|
style: `background-color: ${color};`
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startLine.number === endLine.number) {
|
||||||
|
// selected content in a single line.
|
||||||
|
decorations.push({
|
||||||
|
from: start,
|
||||||
|
to: end,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// selected content in multiple lines
|
||||||
|
// first, render text-selection in the first line
|
||||||
|
decorations.push({
|
||||||
|
from: start,
|
||||||
|
to: startLine.from + startLine.length,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// render text-selection in the lines between the first and last line
|
||||||
|
for (let i = startLine.number + 1; i < endLine.number; i++) {
|
||||||
|
const currentLine = update.view.state.doc.line(i);
|
||||||
|
decorations.push({
|
||||||
|
from: currentLine.from,
|
||||||
|
to: currentLine.to,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// render text-selection in the last line
|
||||||
|
decorations.push({
|
||||||
|
from: endLine.from,
|
||||||
|
to: end,
|
||||||
|
value: Decoration.mark({
|
||||||
|
attributes
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
decorations.push({
|
||||||
|
from: end,
|
||||||
|
to: end,
|
||||||
|
value: Decoration.widget({
|
||||||
|
side: end - start > 0 ? -1 : 1, // the local cursor should be rendered outside the remote selection
|
||||||
|
block: false,
|
||||||
|
widget: new RemoteCursorWidget(color, name)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.decorations = Decoration.set(decorations, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remoteCursorsPlugin = ViewPlugin.fromClass(
|
||||||
|
RemoteCursorsPluginValue,
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function setCursors(clients: ClientCursors[], app: App): void {
|
||||||
|
cursors = clients.flatMap((client) => {
|
||||||
|
const clientCursors = client.cursors;
|
||||||
|
return Object.keys(clientCursors).flatMap((path) => {
|
||||||
|
const spans = clientCursors[path];
|
||||||
|
return spans
|
||||||
|
? spans.map((span) => ({
|
||||||
|
name: client.userName,
|
||||||
|
path,
|
||||||
|
span
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.workspace
|
||||||
|
.getLeavesOfType("markdown")
|
||||||
|
.map((leaf) => leaf.view)
|
||||||
|
.filter((view) => view instanceof MarkdownView)
|
||||||
|
.forEach((view) => {
|
||||||
|
// @ts-expect-error, not typed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
const editor = view.editor.cm as EditorView;
|
||||||
|
|
||||||
|
editor.dispatch({
|
||||||
|
effects: [forceUpdate.of(null)]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -18,8 +18,8 @@ export class HistoryView extends ItemView {
|
||||||
>();
|
>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
leaf: WorkspaceLeaf,
|
private readonly client: SyncClient,
|
||||||
private readonly client: SyncClient
|
leaf: WorkspaceLeaf
|
||||||
) {
|
) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
this.icon = HistoryView.ICON;
|
this.icon = HistoryView.ICON;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export class StatusDescription {
|
||||||
private lastRemaining: number | undefined;
|
private lastRemaining: number | undefined;
|
||||||
private lastConnectionState: NetworkConnectionStatus | undefined;
|
private lastConnectionState: NetworkConnectionStatus | undefined;
|
||||||
|
|
||||||
private statusChangeListeners: (() => void)[] = [];
|
private statusChangeListeners: (() => unknown)[] = [];
|
||||||
|
|
||||||
public constructor(private readonly syncClient: SyncClient) {
|
public constructor(private readonly syncClient: SyncClient) {
|
||||||
void this.updateConnectionState();
|
void this.updateConnectionState();
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,16 @@ module.exports = (env, argv) => ({
|
||||||
ignored: "**/node_modules"
|
ignored: "**/node_modules"
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
obsidian: "commonjs obsidian"
|
obsidian: "commonjs obsidian",
|
||||||
|
electron: "commonjs electron",
|
||||||
|
"@codemirror/autocomplete": "commonjs @codemirror/autocomplete",
|
||||||
|
"@codemirror/collab": "commonjs @codemirror/collab",
|
||||||
|
"@codemirror/commands": "commonjs @codemirror/commands",
|
||||||
|
"@codemirror/language": "commonjs @codemirror/language",
|
||||||
|
"@codemirror/lint": "commonjs @codemirror/lint",
|
||||||
|
"@codemirror/search": "commonjs @codemirror/search",
|
||||||
|
"@codemirror/state": "commonjs @codemirror/state",
|
||||||
|
"@codemirror/view": "commonjs @codemirror/view"
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: [
|
minimizer: [
|
||||||
|
|
|
||||||
484
frontend/package-lock.json
generated
484
frontend/package-lock.json
generated
|
|
@ -12,11 +12,11 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"eslint": "9.23.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"npm-check-updates": "^17.1.16",
|
"npm-check-updates": "^18.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript-eslint": "8.32.1"
|
"typescript-eslint": "8.33.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../backend/sync_lib/pkg": {
|
"../backend/sync_lib/pkg": {
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"version": "7.26.2",
|
"version": "7.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
|
|
@ -204,6 +205,7 @@
|
||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
|
@ -630,9 +632,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.19.2",
|
"version": "0.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||||
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
|
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -655,9 +657,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.12.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
||||||
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
|
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -692,13 +694,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.23.0",
|
"version": "9.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz",
|
||||||
"integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==",
|
"integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://eslint.org/donate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/object-schema": {
|
"node_modules/@eslint/object-schema": {
|
||||||
|
|
@ -712,13 +717,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.2.7",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
||||||
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
|
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.12.0",
|
"@eslint/core": "^0.14.0",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1620,76 +1625,6 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@redocly/ajv": {
|
|
||||||
"version": "8.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz",
|
|
||||||
"integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.1",
|
|
||||||
"json-schema-traverse": "^1.0.0",
|
|
||||||
"require-from-string": "^2.0.2",
|
|
||||||
"uri-js-replace": "^1.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@redocly/config": {
|
|
||||||
"version": "0.22.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.1.tgz",
|
|
||||||
"integrity": "sha512-1CqQfiG456v9ZgYBG9xRQHnpXjt8WoSnDwdkX6gxktuK69v2037hTAR1eh0DGIqpZ1p4k82cGH8yTNwt7/pI9g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@redocly/openapi-core": {
|
|
||||||
"version": "1.34.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.0.tgz",
|
|
||||||
"integrity": "sha512-Ji00EiLQRXq0pJIz5pAjGF9MfQvQVsQehc6uIis6sqat8tG/zh25Zi64w6HVGEDgJEzUeq/CuUlD0emu3Hdaqw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@redocly/ajv": "^8.11.2",
|
|
||||||
"@redocly/config": "^0.22.0",
|
|
||||||
"colorette": "^1.2.0",
|
|
||||||
"https-proxy-agent": "^7.0.5",
|
|
||||||
"js-levenshtein": "^1.1.6",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"minimatch": "^5.0.1",
|
|
||||||
"pluralize": "^8.0.0",
|
|
||||||
"yaml-ast-parser": "0.0.43"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.17.0",
|
|
||||||
"npm": ">=9.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@redocly/openapi-core/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
|
|
@ -1857,9 +1792,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.27",
|
"version": "22.15.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
|
||||||
"integrity": "sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==",
|
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1901,17 +1836,17 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz",
|
||||||
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
|
"integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.33.1",
|
||||||
"@typescript-eslint/type-utils": "8.32.1",
|
"@typescript-eslint/type-utils": "8.33.1",
|
||||||
"@typescript-eslint/utils": "8.32.1",
|
"@typescript-eslint/utils": "8.33.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/visitor-keys": "8.33.1",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
|
|
@ -1925,7 +1860,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
"@typescript-eslint/parser": "^8.33.1",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1941,16 +1876,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz",
|
||||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
"integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.33.1",
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.33.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
"@typescript-eslint/typescript-estree": "8.33.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/visitor-keys": "8.33.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1965,15 +1900,37 @@
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz",
|
||||||
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
|
"integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/tsconfig-utils": "^8.33.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1"
|
"@typescript-eslint/types": "^8.33.1",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
|
"version": "8.33.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz",
|
||||||
|
"integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.33.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.33.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1983,15 +1940,32 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
|
"version": "8.33.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz",
|
||||||
|
"integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz",
|
||||||
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
|
"integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
"@typescript-eslint/typescript-estree": "8.33.1",
|
||||||
"@typescript-eslint/utils": "8.32.1",
|
"@typescript-eslint/utils": "8.33.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2008,9 +1982,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz",
|
||||||
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
|
"integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2022,14 +1996,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz",
|
||||||
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
|
"integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/project-service": "8.33.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/tsconfig-utils": "8.33.1",
|
||||||
|
"@typescript-eslint/types": "8.33.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.33.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
|
|
@ -2075,16 +2051,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz",
|
||||||
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
|
"integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.33.1",
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.33.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1"
|
"@typescript-eslint/typescript-estree": "8.33.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2099,13 +2075,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz",
|
||||||
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
|
"integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.33.1",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2375,15 +2351,6 @@
|
||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
|
||||||
"version": "7.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
|
||||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|
@ -2453,15 +2420,6 @@
|
||||||
"ajv": "^6.9.1"
|
"ajv": "^6.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-colors": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
|
|
@ -2522,6 +2480,7 @@
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
|
|
@ -2882,12 +2841,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/change-case": {
|
|
||||||
"version": "5.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
|
||||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/char-regex": {
|
"node_modules/char-regex": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
||||||
|
|
@ -3015,12 +2968,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/colorette": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
|
@ -3169,6 +3116,7 @@
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
|
|
@ -3417,20 +3365,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.23.0",
|
"version": "9.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz",
|
||||||
"integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==",
|
"integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.19.2",
|
"@eslint/config-array": "^0.20.0",
|
||||||
"@eslint/config-helpers": "^0.2.0",
|
"@eslint/config-helpers": "^0.2.1",
|
||||||
"@eslint/core": "^0.12.0",
|
"@eslint/core": "^0.14.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.23.0",
|
"@eslint/js": "9.28.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
|
|
@ -3671,6 +3619,7 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
|
|
@ -4146,19 +4095,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
|
||||||
"version": "7.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"agent-base": "^7.1.2",
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
|
|
@ -4246,18 +4182,6 @@
|
||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/index-to-position": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
|
@ -5076,25 +5000,18 @@
|
||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-levenshtein": {
|
|
||||||
"version": "1.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
|
||||||
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
|
|
@ -5515,6 +5432,7 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
|
|
@ -5595,9 +5513,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-check-updates": {
|
"node_modules/npm-check-updates": {
|
||||||
"version": "17.1.16",
|
"version": "18.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz",
|
||||||
"integrity": "sha512-9nohkfjLRzLfsLVGbO34eXBejvrOOTuw5tvNammH73KEFG5XlFoi3G2TgjTExHtnrKWCbZ+mTT+dbNeSjASIPw==",
|
"integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -5676,82 +5594,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openapi-fetch": {
|
|
||||||
"version": "0.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.0.tgz",
|
|
||||||
"integrity": "sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"openapi-typescript-helpers": "^0.0.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openapi-typescript": {
|
|
||||||
"version": "7.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.6.1.tgz",
|
|
||||||
"integrity": "sha512-F7RXEeo/heF3O9lOXo2bNjCOtfp7u+D6W3a3VNEH2xE6v+fxLtn5nq0uvUcA1F5aT+CMhNeC5Uqtg5tlXFX/ag==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@redocly/openapi-core": "^1.28.0",
|
|
||||||
"ansi-colors": "^4.1.3",
|
|
||||||
"change-case": "^5.4.4",
|
|
||||||
"parse-json": "^8.1.0",
|
|
||||||
"supports-color": "^9.4.0",
|
|
||||||
"yargs-parser": "^21.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"openapi-typescript": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openapi-typescript-helpers": {
|
|
||||||
"version": "0.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz",
|
|
||||||
"integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/openapi-typescript/node_modules/parse-json": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/code-frame": "^7.26.2",
|
|
||||||
"index-to-position": "^1.0.0",
|
|
||||||
"type-fest": "^4.37.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openapi-typescript/node_modules/supports-color": {
|
|
||||||
"version": "9.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
|
|
||||||
"integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openapi-typescript/node_modules/type-fest": {
|
|
||||||
"version": "4.38.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz",
|
|
||||||
"integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==",
|
|
||||||
"license": "(MIT OR CC0-1.0)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
@ -5913,6 +5755,7 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
|
|
@ -6007,15 +5850,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pluralize": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
|
|
@ -6333,6 +6167,7 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|
@ -6493,9 +6328,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.89.0",
|
"version": "1.89.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz",
|
||||||
"integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==",
|
"integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -7276,6 +7111,7 @@
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
@ -7286,15 +7122,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.32.1",
|
"version": "8.33.1",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz",
|
||||||
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
|
"integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
"@typescript-eslint/eslint-plugin": "8.33.1",
|
||||||
"@typescript-eslint/parser": "8.32.1",
|
"@typescript-eslint/parser": "8.33.1",
|
||||||
"@typescript-eslint/utils": "8.32.1"
|
"@typescript-eslint/utils": "8.33.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -7366,12 +7202,6 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uri-js-replace": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/url": {
|
"node_modules/url": {
|
||||||
"version": "0.11.4",
|
"version": "0.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
||||||
|
|
@ -7491,14 +7321,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.98.0",
|
"version": "5.99.9",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz",
|
||||||
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
|
"integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@webassemblyjs/ast": "^1.14.1",
|
"@webassemblyjs/ast": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||||
|
|
@ -7515,7 +7346,7 @@
|
||||||
"loader-runner": "^4.2.0",
|
"loader-runner": "^4.2.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"schema-utils": "^4.3.0",
|
"schema-utils": "^4.3.2",
|
||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.11",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
|
|
@ -7684,9 +7515,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/schema-utils": {
|
"node_modules/webpack/node_modules/schema-utils": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
|
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -7814,12 +7645,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml-ast-parser": {
|
|
||||||
"version": "0.0.43",
|
|
||||||
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
|
||||||
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|
@ -7843,6 +7668,7 @@
|
||||||
"version": "21.1.1",
|
"version": "21.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|
@ -7867,7 +7693,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
|
@ -7876,7 +7702,7 @@
|
||||||
"mini-css-extract-plugin": "^2.9.2",
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
"obsidian": "1.8.7",
|
"obsidian": "1.8.7",
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sass": "^1.89.0",
|
"sass": "^1.89.1",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"sync-client": "file:../sync-client",
|
"sync-client": "file:../sync-client",
|
||||||
"terser-webpack-plugin": "^5.3.14",
|
"terser-webpack-plugin": "^5.3.14",
|
||||||
|
|
@ -7886,7 +7712,7 @@
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"url": "^0.11.4",
|
"url": "^0.11.4",
|
||||||
"virtual-scroller": "^1.13.1",
|
"virtual-scroller": "^1.13.1",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -7895,21 +7721,19 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"byte-base64": "^1.1.0",
|
"byte-base64": "^1.1.0",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
"openapi-fetch": "0.14.0",
|
|
||||||
"openapi-typescript": "7.6.1",
|
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"sync_lib": "file:../../backend/sync_lib/pkg",
|
"sync_lib": "file:../../backend/sync_lib/pkg",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-merge": "^6.0.1",
|
"webpack-merge": "^6.0.1",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
|
|
@ -7945,14 +7769,14 @@
|
||||||
"test-client": "dist/cli.js"
|
"test-client": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"sync-client": "file:../sync-client",
|
"sync-client": "file:../sync-client",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"eslint": "9.23.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"npm-check-updates": "^17.1.16",
|
"npm-check-updates": "^18.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript-eslint": "8.32.1"
|
"typescript-eslint": "8.33.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,23 +15,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"byte-base64": "^1.1.0",
|
"byte-base64": "^1.1.0",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
"openapi-fetch": "0.14.0",
|
|
||||||
"openapi-typescript": "7.6.1",
|
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"sync_lib": "file:../../backend/sync_lib/pkg",
|
"sync_lib": "file:../../backend/sync_lib/pkg",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-merge": "^6.0.1",
|
"webpack-merge": "^6.0.1",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,8 @@ export type {
|
||||||
Cursor
|
Cursor
|
||||||
} from "./file-operations/filesystem-operations";
|
} from "./file-operations/filesystem-operations";
|
||||||
export type { PersistenceProvider } from "./persistence/persistence";
|
export type { PersistenceProvider } from "./persistence/persistence";
|
||||||
|
export type { CursorSpan } from "./services/types/CursorSpan";
|
||||||
|
export type { ClientCursors } from "./services/types/ClientCursors";
|
||||||
export type { NetworkConnectionStatus } from "./types/network-connection-status";
|
export type { NetworkConnectionStatus } from "./types/network-connection-status";
|
||||||
export { DocumentUpdateStatus } from "./types/document-update-status";
|
export { DocumentUpdateStatus } from "./types/document-update-status";
|
||||||
export { SyncClient } from "./sync-client";
|
export { SyncClient } from "./sync-client";
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface SyncSettings {
|
||||||
isSyncEnabled: boolean;
|
isSyncEnabled: boolean;
|
||||||
maxFileSizeMB: number;
|
maxFileSizeMB: number;
|
||||||
ignorePatterns: string[];
|
ignorePatterns: string[];
|
||||||
|
webSocketRetryIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: SyncSettings = {
|
export const DEFAULT_SETTINGS: SyncSettings = {
|
||||||
|
|
@ -17,7 +18,8 @@ export const DEFAULT_SETTINGS: SyncSettings = {
|
||||||
syncConcurrency: 1,
|
syncConcurrency: 1,
|
||||||
isSyncEnabled: false,
|
isSyncEnabled: false,
|
||||||
maxFileSizeMB: 10,
|
maxFileSizeMB: 10,
|
||||||
ignorePatterns: []
|
ignorePatterns: [],
|
||||||
|
webSocketRetryIntervalMs: 3500
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,10 @@ export class ConnectionStatus {
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
fetch: typeof globalThis.fetch = globalThis.fetch
|
fetch: typeof globalThis.fetch = globalThis.fetch
|
||||||
): typeof globalThis.fetch {
|
): typeof globalThis.fetch {
|
||||||
return async (input: RequestInfo | URL): Promise<Response> => {
|
return async (
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit
|
||||||
|
): Promise<Response> => {
|
||||||
while (!this.canFetch) {
|
while (!this.canFetch) {
|
||||||
await this.until;
|
await this.until;
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +66,7 @@ export class ConnectionStatus {
|
||||||
? input.clone()
|
? input.clone()
|
||||||
: input;
|
: input;
|
||||||
|
|
||||||
const fetchPromise = fetch(_input);
|
const fetchPromise = fetch(_input, init);
|
||||||
|
|
||||||
// We only want to catch rejections from `this.until`
|
// We only want to catch rejections from `this.until`
|
||||||
let result: symbol | Response | undefined = undefined;
|
let result: symbol | Response | undefined = undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import type { Client } from "openapi-fetch";
|
|
||||||
import createClient from "openapi-fetch";
|
|
||||||
import type { components, paths } from "./types"; // generated by openapi-typescript
|
|
||||||
import type {
|
import type {
|
||||||
DocumentId,
|
DocumentId,
|
||||||
RelativePath,
|
RelativePath,
|
||||||
VaultUpdateId
|
VaultUpdateId
|
||||||
} from "../persistence/database";
|
} from "../persistence/database";
|
||||||
|
|
||||||
import type { Logger } from "../tracing/logger";
|
import type { Logger } from "../tracing/logger";
|
||||||
import type { Settings } from "../persistence/settings";
|
import type { Settings } from "../persistence/settings";
|
||||||
import type { ConnectionStatus } from "./connection-status";
|
import type { ConnectionStatus } from "./connection-status";
|
||||||
import { sleep } from "../utils/sleep";
|
import { sleep } from "../utils/sleep";
|
||||||
import { SyncResetError } from "./sync-reset-error";
|
import { SyncResetError } from "./sync-reset-error";
|
||||||
|
import type { SerializedError } from "./types/SerializedError";
|
||||||
|
import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent";
|
||||||
|
import type { DocumentUpdateResponse } from "./types/DocumentUpdateResponse";
|
||||||
|
import type { DocumentVersion } from "./types/DocumentVersion";
|
||||||
|
import type { FetchLatestDocumentsResponse } from "./types/FetchLatestDocumentsResponse";
|
||||||
|
import type { PingResponse } from "./types/PingResponse";
|
||||||
|
import type { DeleteDocumentVersion } from "./types/DeleteDocumentVersion";
|
||||||
|
|
||||||
export interface CheckConnectionResult {
|
export interface CheckConnectionResult {
|
||||||
isSuccessful: boolean;
|
isSuccessful: boolean;
|
||||||
|
|
@ -19,47 +24,28 @@ export interface CheckConnectionResult {
|
||||||
|
|
||||||
export class SyncService {
|
export class SyncService {
|
||||||
private static readonly NETWORK_RETRY_INTERVAL_MS = 1000;
|
private static readonly NETWORK_RETRY_INTERVAL_MS = 1000;
|
||||||
private client: Client<paths>;
|
private readonly client: typeof globalThis.fetch;
|
||||||
private pingClient: Client<paths>;
|
private readonly pingClient: typeof globalThis.fetch;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly deviceId: string,
|
private readonly deviceId: string,
|
||||||
private readonly connectionStatus: ConnectionStatus,
|
private readonly connectionStatus: ConnectionStatus,
|
||||||
private readonly settings: Settings,
|
private readonly settings: Settings,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly fetchImplementation: typeof globalThis.fetch = globalThis.fetch
|
fetchImplementation: typeof globalThis.fetch = globalThis.fetch
|
||||||
) {
|
) {
|
||||||
[this.client, this.pingClient] = this.createClient(
|
// ensure that if it's called a method, `this` won't be bound to the instance
|
||||||
this.settings.getSettings().remoteUri
|
const unboundFetch: typeof globalThis.fetch = async (...args) =>
|
||||||
|
fetchImplementation(...args);
|
||||||
|
|
||||||
|
this.client = this.connectionStatus.getFetchImplementation(
|
||||||
|
this.logger,
|
||||||
|
unboundFetch
|
||||||
);
|
);
|
||||||
|
this.pingClient = unboundFetch;
|
||||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
|
||||||
if (newSettings.remoteUri === oldSettings.remoteUri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[this.client, this.pingClient] = this.createClient(
|
|
||||||
newSettings.remoteUri
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get deviceIdHeader(): string {
|
private static formatError(error: SerializedError): string {
|
||||||
// @ts-expect-error, injected by webpack
|
|
||||||
const packageVersion = __CURRENT_VERSION__; // eslint-disable-line
|
|
||||||
|
|
||||||
const platform =
|
|
||||||
typeof navigator !== "undefined"
|
|
||||||
? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated
|
|
||||||
: typeof process !== "undefined"
|
|
||||||
? process.platform
|
|
||||||
: "unknown";
|
|
||||||
return `vault-link/${packageVersion} (${this.deviceId}; ${platform})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static formatError(
|
|
||||||
error: components["schemas"]["SerializedError"]
|
|
||||||
): string {
|
|
||||||
let result = error.message;
|
let result = error.message;
|
||||||
if (error.causes.length > 0) {
|
if (error.causes.length > 0) {
|
||||||
const causes = error.causes.join(", ");
|
const causes = error.causes.join(", ");
|
||||||
|
|
@ -77,47 +63,39 @@ export class SyncService {
|
||||||
documentId?: DocumentId;
|
documentId?: DocumentId;
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
contentBytes: Uint8Array;
|
contentBytes: Uint8Array;
|
||||||
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
}): Promise<DocumentVersionWithoutContent> {
|
||||||
const { vaultName } = this.settings.getSettings();
|
|
||||||
|
|
||||||
return this.withRetries(async () => {
|
return this.withRetries(async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (documentId !== undefined) {
|
if (documentId !== undefined) {
|
||||||
formData.append("document_id", documentId);
|
formData.append("document_id", documentId);
|
||||||
}
|
}
|
||||||
formData.append("relative_path", relativePath);
|
formData.append("relative_path", relativePath);
|
||||||
formData.append("device_id", this.deviceId);
|
|
||||||
formData.append("content", new Blob([contentBytes]));
|
formData.append("content", new Blob([contentBytes]));
|
||||||
|
|
||||||
const response = await this.client.POST(
|
const response = await this.client(this.getUrl("/documents"), {
|
||||||
"/vaults/{vault_id}/documents",
|
method: "POST",
|
||||||
{
|
body: formData,
|
||||||
params: {
|
headers: this.getDefaultHeaders()
|
||||||
path: {
|
});
|
||||||
vault_id: vaultName
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
"device-id": this.deviceIdHeader
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line
|
|
||||||
body: formData as any // FormData is not supported by openapi-fetch
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data) {
|
const result: SerializedError | DocumentVersionWithoutContent =
|
||||||
|
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
| SerializedError
|
||||||
|
| DocumentVersionWithoutContent;
|
||||||
|
|
||||||
|
if ("errorType" in result) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to create document: ${SyncService.formatError(response.error)}`
|
`Failed to create document: ${SyncService.formatError(result)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Created document ${JSON.stringify(response.data)} with id ${
|
`Created document ${JSON.stringify(result)} with id ${
|
||||||
response.data.documentId
|
result.documentId
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,9 +109,7 @@ export class SyncService {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
contentBytes: Uint8Array;
|
contentBytes: Uint8Array;
|
||||||
}): Promise<components["schemas"]["DocumentUpdateResponse"]> {
|
}): Promise<DocumentUpdateResponse> {
|
||||||
const { vaultName } = this.settings.getSettings();
|
|
||||||
|
|
||||||
return this.withRetries(async () => {
|
return this.withRetries(async () => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updating document ${documentId} with parent version ${parentVersionId} and relative path ${relativePath}`
|
`Updating document ${documentId} with parent version ${parentVersionId} and relative path ${relativePath}`
|
||||||
|
|
@ -141,39 +117,35 @@ export class SyncService {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("parent_version_id", parentVersionId.toString());
|
formData.append("parent_version_id", parentVersionId.toString());
|
||||||
formData.append("relative_path", relativePath);
|
formData.append("relative_path", relativePath);
|
||||||
formData.append("device_id", this.deviceId);
|
|
||||||
formData.append("content", new Blob([contentBytes]));
|
formData.append("content", new Blob([contentBytes]));
|
||||||
|
|
||||||
const response = await this.client.PUT(
|
const response = await this.client(
|
||||||
"/vaults/{vault_id}/documents/{document_id}",
|
this.getUrl(`/documents/${documentId}`),
|
||||||
{
|
{
|
||||||
params: {
|
method: "PUT",
|
||||||
path: {
|
body: formData,
|
||||||
vault_id: vaultName,
|
headers: this.getDefaultHeaders()
|
||||||
document_id: documentId
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
"device-id": this.deviceIdHeader
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line
|
|
||||||
body: formData as any // FormData is not supported by openapi-fetch
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.data) {
|
const result: SerializedError | DocumentUpdateResponse =
|
||||||
|
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
| SerializedError
|
||||||
|
| DocumentUpdateResponse;
|
||||||
|
|
||||||
|
if ("errorType" in result) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to update document: ${SyncService.formatError(response.error)}`
|
`Failed to update document: ${SyncService.formatError(result)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updated document ${JSON.stringify(response.data)} with id ${
|
`Updated document ${JSON.stringify(result)} with id ${
|
||||||
response.data.documentId
|
result.documentId
|
||||||
}`
|
}}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,39 +155,39 @@ export class SyncService {
|
||||||
}: {
|
}: {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
}): Promise<DocumentVersionWithoutContent> {
|
||||||
return this.withRetries(async () => {
|
return this.withRetries(async () => {
|
||||||
const { vaultName } = this.settings.getSettings();
|
const request: DeleteDocumentVersion = {
|
||||||
|
relativePath
|
||||||
const response = await this.client.DELETE(
|
};
|
||||||
"/vaults/{vault_id}/documents/{document_id}",
|
const response = await this.client(
|
||||||
|
this.getUrl(`/documents/${documentId}`),
|
||||||
{
|
{
|
||||||
params: {
|
method: "DELETE",
|
||||||
path: {
|
body: JSON.stringify(request),
|
||||||
vault_id: vaultName,
|
headers: {
|
||||||
document_id: documentId
|
"Content-Type": "application/json",
|
||||||
},
|
...this.getDefaultHeaders()
|
||||||
header: {
|
|
||||||
"device-id": this.deviceIdHeader
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
body: {
|
|
||||||
relativePath,
|
|
||||||
deviceId: this.deviceId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.error) {
|
const result: SerializedError | DocumentVersionWithoutContent =
|
||||||
throw new Error(`Failed to delete document`);
|
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
| SerializedError
|
||||||
|
| DocumentVersionWithoutContent;
|
||||||
|
|
||||||
|
if ("errorType" in result) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to delete document: ${SyncService.formatError(result)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Deleted document ${relativePath} with id ${documentId}`
|
`Deleted document ${relativePath} with id ${documentId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,100 +195,77 @@ export class SyncService {
|
||||||
documentId
|
documentId
|
||||||
}: {
|
}: {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
}): Promise<components["schemas"]["DocumentVersion"]> {
|
}): Promise<DocumentVersion> {
|
||||||
const { vaultName } = this.settings.getSettings();
|
|
||||||
|
|
||||||
return this.withRetries(async () => {
|
return this.withRetries(async () => {
|
||||||
const response = await this.client.GET(
|
const response = await this.client(
|
||||||
"/vaults/{vault_id}/documents/{document_id}",
|
this.getUrl(`/documents/${documentId}`),
|
||||||
{
|
{
|
||||||
params: {
|
headers: this.getDefaultHeaders()
|
||||||
path: {
|
|
||||||
vault_id: vaultName,
|
|
||||||
document_id: documentId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.data) {
|
const result: SerializedError | DocumentVersion =
|
||||||
|
(await response.json()) as SerializedError | DocumentVersion; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
|
||||||
|
if ("errorType" in result) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to get document: ${SyncService.formatError(response.error)}`
|
`Failed to get document: ${SyncService.formatError(result)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Get document ${response.data.relativePath} with id ${response.data.documentId}`
|
`Get document ${result.relativePath} with id ${result.documentId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAll(
|
public async getAll(
|
||||||
since?: VaultUpdateId
|
since?: VaultUpdateId
|
||||||
): Promise<components["schemas"]["FetchLatestDocumentsResponse"]> {
|
): Promise<FetchLatestDocumentsResponse> {
|
||||||
return this.withRetries(async () => {
|
return this.withRetries(async () => {
|
||||||
const { vaultName } = this.settings.getSettings();
|
const url = new URL(this.getUrl("/documents"));
|
||||||
|
if (since !== undefined) {
|
||||||
|
url.searchParams.append("since", since.toString());
|
||||||
|
}
|
||||||
|
const response = await this.client(url.toString(), {
|
||||||
|
headers: this.getDefaultHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
const response = await this.client.GET(
|
const result: SerializedError | FetchLatestDocumentsResponse =
|
||||||
"/vaults/{vault_id}/documents",
|
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
{
|
| SerializedError
|
||||||
params: {
|
| FetchLatestDocumentsResponse;
|
||||||
path: {
|
|
||||||
vault_id: vaultName
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
since_update_id: since
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { error } = response;
|
if ("errorType" in result) {
|
||||||
if (error) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to get documents: ${SyncService.formatError(response.error)}`
|
`Failed to get documents: ${SyncService.formatError(result)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Got ${response.data.latestDocuments.length} document metadata`
|
`Got ${result.latestDocuments.length} document metadata`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkConnection(): Promise<CheckConnectionResult> {
|
public async checkConnection(): Promise<CheckConnectionResult> {
|
||||||
const { vaultName } = this.settings.getSettings();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.pingClient.GET(
|
const response = await this.pingClient(this.getUrl("/ping"), {
|
||||||
"/vaults/{vault_id}/ping",
|
headers: this.getDefaultHeaders()
|
||||||
{
|
});
|
||||||
params: {
|
const result: PingResponse | SerializedError =
|
||||||
header: {
|
(await response.json()) as PingResponse | SerializedError; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
authorization: `Bearer ${this.settings.getSettings().token}`
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
vault_id: vaultName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.debug(
|
if ("errorType" in result) {
|
||||||
`Ping response: ${JSON.stringify(response.data)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to ping server: ${SyncService.formatError(response.error)}`
|
`Failed to ping server: ${SyncService.formatError(result)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = response.data;
|
|
||||||
if (result.isAuthenticated) {
|
if (result.isAuthenticated) {
|
||||||
return {
|
return {
|
||||||
isSuccessful: true,
|
isSuccessful: true,
|
||||||
|
|
@ -336,29 +285,17 @@ export class SyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getUrl(path: string): string {
|
||||||
* Create a client and a ping client for the given remote URI.
|
const { vaultName, remoteUri } = this.settings.getSettings();
|
||||||
*/
|
const safeRemoteUri = remoteUri.replace(/\/+$/, "");
|
||||||
private createClient(remoteUri: string): [Client<paths>, Client<paths>] {
|
return `${safeRemoteUri}/vaults/${vaultName}${path}`;
|
||||||
return [
|
}
|
||||||
createClient<paths>({
|
|
||||||
baseUrl: remoteUri,
|
private getDefaultHeaders(): Record<string, string> {
|
||||||
fetch: this.connectionStatus.getFetchImplementation(
|
return {
|
||||||
this.logger,
|
"device-id": this.deviceId,
|
||||||
this.fetchImplementation
|
authorization: `Bearer ${this.settings.getSettings().token}`
|
||||||
),
|
};
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${this.settings.getSettings().token}`
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createClient<paths>({
|
|
||||||
baseUrl: remoteUri,
|
|
||||||
fetch: this.fetchImplementation,
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${this.settings.getSettings().token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async withRetries<T>(fn: () => Promise<T>): Promise<T> {
|
private async withRetries<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
|
|
||||||
|
|
@ -1,655 +0,0 @@
|
||||||
/**
|
|
||||||
* This file was auto-generated by openapi-typescript.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface paths {
|
|
||||||
"/vaults/{vault_id}/documents": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get: {
|
|
||||||
parameters: {
|
|
||||||
query?: {
|
|
||||||
since_update_id?: number | null;
|
|
||||||
};
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["FetchLatestDocumentsResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
put?: never;
|
|
||||||
post: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header: {
|
|
||||||
"device-id": string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"multipart/form-data": components["schemas"]["CreateDocumentVersionMultipart"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/documents/json": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header: {
|
|
||||||
"device-id": string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["CreateDocumentVersion"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/documents/{document_id}": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentVersion"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header: {
|
|
||||||
"device-id": string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"multipart/form-data": components["schemas"]["UpdateDocumentVersionMultipart"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentUpdateResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
post?: never;
|
|
||||||
delete: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header: {
|
|
||||||
"device-id": string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DeleteDocumentVersion"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/documents/{document_id}/json": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header: {
|
|
||||||
"device-id": string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["UpdateDocumentVersion"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentUpdateResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
post?: never;
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/documents/{document_id}/versions/{version_id}": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
vault_update_id: number;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["DocumentVersion"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
post?: never;
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/documents/{document_id}/versions/{version_id}/content": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
vault_update_id: number;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description byte stream */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/octet-stream": unknown;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
post?: never;
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/vaults/{vault_id}/ping": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: {
|
|
||||||
authorization?: string;
|
|
||||||
};
|
|
||||||
path: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["PingResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
/** @example {
|
|
||||||
* "causes": [],
|
|
||||||
* "message": "An error has occurred"
|
|
||||||
* } */
|
|
||||||
"application/json": components["schemas"]["SerializedError"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
put?: never;
|
|
||||||
post?: never;
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export type webhooks = Record<string, never>;
|
|
||||||
export interface components {
|
|
||||||
schemas: {
|
|
||||||
Array_of_uint8: number[];
|
|
||||||
CreateDocumentPathParams: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
CreateDocumentVersion: {
|
|
||||||
contentBase64: string;
|
|
||||||
deviceId?: string | null;
|
|
||||||
/**
|
|
||||||
* Format: uuid
|
|
||||||
* @description The client can decide the document id (if it wishes to) in order to help with syncing. If the client does not provide a document id, the server will generate one. If the client provides a document id it must not already exist in the database.
|
|
||||||
*/
|
|
||||||
documentId?: string | null;
|
|
||||||
relativePath: string;
|
|
||||||
};
|
|
||||||
CreateDocumentVersionMultipart: {
|
|
||||||
content: components["schemas"]["Array_of_uint8"];
|
|
||||||
device_id?: string | null;
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id?: string | null;
|
|
||||||
relative_path: string;
|
|
||||||
};
|
|
||||||
DeleteDocumentPathParams: {
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
DeleteDocumentVersion: {
|
|
||||||
deviceId?: string | null;
|
|
||||||
relativePath: string;
|
|
||||||
};
|
|
||||||
/** @description Response to an update document request. */
|
|
||||||
DocumentUpdateResponse:
|
|
||||||
| {
|
|
||||||
/** Format: uint64 */
|
|
||||||
contentSize: number;
|
|
||||||
deviceId: string;
|
|
||||||
/** Format: uuid */
|
|
||||||
documentId: string;
|
|
||||||
isDeleted: boolean;
|
|
||||||
relativePath: string;
|
|
||||||
/** @enum {string} */
|
|
||||||
type: "FastForwardUpdate";
|
|
||||||
/** Format: date-time */
|
|
||||||
updatedDate: string;
|
|
||||||
userId: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vaultUpdateId: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
contentBase64: string;
|
|
||||||
deviceId: string;
|
|
||||||
/** Format: uuid */
|
|
||||||
documentId: string;
|
|
||||||
isDeleted: boolean;
|
|
||||||
relativePath: string;
|
|
||||||
/** @enum {string} */
|
|
||||||
type: "MergingUpdate";
|
|
||||||
/** Format: date-time */
|
|
||||||
updatedDate: string;
|
|
||||||
userId: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vaultUpdateId: number;
|
|
||||||
};
|
|
||||||
DocumentVersion: {
|
|
||||||
contentBase64: string;
|
|
||||||
deviceId: string;
|
|
||||||
/** Format: uuid */
|
|
||||||
documentId: string;
|
|
||||||
isDeleted: boolean;
|
|
||||||
relativePath: string;
|
|
||||||
/** Format: date-time */
|
|
||||||
updatedDate: string;
|
|
||||||
userId: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vaultUpdateId: number;
|
|
||||||
};
|
|
||||||
DocumentVersionWithoutContent: {
|
|
||||||
/** Format: uint64 */
|
|
||||||
contentSize: number;
|
|
||||||
deviceId: string;
|
|
||||||
/** Format: uuid */
|
|
||||||
documentId: string;
|
|
||||||
isDeleted: boolean;
|
|
||||||
relativePath: string;
|
|
||||||
/** Format: date-time */
|
|
||||||
updatedDate: string;
|
|
||||||
userId: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vaultUpdateId: number;
|
|
||||||
};
|
|
||||||
FetchDocumentVersionContentPathParams: {
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vault_update_id: number;
|
|
||||||
};
|
|
||||||
FetchDocumentVersionPathParams: {
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
/** Format: int64 */
|
|
||||||
vault_update_id: number;
|
|
||||||
};
|
|
||||||
FetchLatestDocumentVersionPathParams: {
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
FetchLatestDocumentsPathParams: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
/** @description Response to a fetch latest documents request. */
|
|
||||||
FetchLatestDocumentsResponse: {
|
|
||||||
/**
|
|
||||||
* Format: int64
|
|
||||||
* @description The update ID of the latest document in the response.
|
|
||||||
*/
|
|
||||||
lastUpdateId: number;
|
|
||||||
latestDocuments: components["schemas"]["DocumentVersionWithoutContent"][];
|
|
||||||
};
|
|
||||||
PingPathParams: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
/** @description Response to a ping request. */
|
|
||||||
PingResponse: {
|
|
||||||
/** @description Whether the client is authenticated based on the sent Authorization header. */
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
/** @description Semantic version of the server. */
|
|
||||||
serverVersion: string;
|
|
||||||
};
|
|
||||||
QueryParams: {
|
|
||||||
/** Format: int64 */
|
|
||||||
since_update_id?: number | null;
|
|
||||||
};
|
|
||||||
SerializedError: {
|
|
||||||
causes: string[];
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
UpdateDocumentPathParams: {
|
|
||||||
/** Format: uuid */
|
|
||||||
document_id: string;
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
UpdateDocumentVersion: {
|
|
||||||
contentBase64: string;
|
|
||||||
deviceId?: string | null;
|
|
||||||
/** Format: int64 */
|
|
||||||
parentVersionId: number;
|
|
||||||
relativePath: string;
|
|
||||||
};
|
|
||||||
UpdateDocumentVersionMultipart: {
|
|
||||||
content: components["schemas"]["Array_of_uint8"];
|
|
||||||
deviceId?: string | null;
|
|
||||||
/** Format: int64 */
|
|
||||||
parentVersionId: number;
|
|
||||||
relativePath: string;
|
|
||||||
};
|
|
||||||
WebsocketPathParams: {
|
|
||||||
vault_id: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: never;
|
|
||||||
parameters: never;
|
|
||||||
requestBodies: never;
|
|
||||||
headers: never;
|
|
||||||
pathItems: never;
|
|
||||||
}
|
|
||||||
export type $defs = Record<string, never>;
|
|
||||||
export type operations = Record<string, never>;
|
|
||||||
8
frontend/sync-client/src/services/types/ClientCursors.ts
Normal file
8
frontend/sync-client/src/services/types/ClientCursors.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { CursorSpan } from "./CursorSpan";
|
||||||
|
|
||||||
|
export interface ClientCursors {
|
||||||
|
userName: string;
|
||||||
|
deviceId: string;
|
||||||
|
cursors: Partial<Record<string, CursorSpan[]>>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface CreateDocumentVersion {
|
||||||
|
/**
|
||||||
|
* The client can decide the document id (if it wishes to) in order
|
||||||
|
* to help with syncing. If the client does not provide a document id,
|
||||||
|
* the server will generate one. If the client provides a document id
|
||||||
|
* it must not already exist in the database.
|
||||||
|
*/
|
||||||
|
document_id: string | null;
|
||||||
|
relative_path: string;
|
||||||
|
content: number[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { CursorSpan } from "./CursorSpan";
|
||||||
|
|
||||||
|
export interface CursorPositionFromClient {
|
||||||
|
documentToCursors: Partial<Record<string, CursorSpan[]>>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ClientCursors } from "./ClientCursors";
|
||||||
|
|
||||||
|
export interface CursorPositionFromServer {
|
||||||
|
clients: ClientCursors[];
|
||||||
|
}
|
||||||
6
frontend/sync-client/src/services/types/CursorSpan.ts
Normal file
6
frontend/sync-client/src/services/types/CursorSpan.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface CursorSpan {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface DeleteDocumentVersion {
|
||||||
|
relativePath: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DocumentVersion } from "./DocumentVersion";
|
||||||
|
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to an update document request.
|
||||||
|
*/
|
||||||
|
export type DocumentUpdateResponse =
|
||||||
|
| ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent)
|
||||||
|
| ({ type: "MergingUpdate" } & DocumentVersion);
|
||||||
12
frontend/sync-client/src/services/types/DocumentVersion.ts
Normal file
12
frontend/sync-client/src/services/types/DocumentVersion.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface DocumentVersion {
|
||||||
|
vaultUpdateId: number;
|
||||||
|
documentId: string;
|
||||||
|
relativePath: string;
|
||||||
|
updatedDate: string;
|
||||||
|
contentBase64: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
userId: string;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface DocumentVersionWithoutContent {
|
||||||
|
vaultUpdateId: number;
|
||||||
|
documentId: string;
|
||||||
|
relativePath: string;
|
||||||
|
updatedDate: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
userId: string;
|
||||||
|
deviceId: string;
|
||||||
|
contentSize: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to a fetch latest documents request.
|
||||||
|
*/
|
||||||
|
export interface FetchLatestDocumentsResponse {
|
||||||
|
latestDocuments: DocumentVersionWithoutContent[];
|
||||||
|
/**
|
||||||
|
* The update ID of the latest document in the response.
|
||||||
|
*/
|
||||||
|
lastUpdateId: bigint;
|
||||||
|
}
|
||||||
16
frontend/sync-client/src/services/types/PingResponse.ts
Normal file
16
frontend/sync-client/src/services/types/PingResponse.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to a ping request.
|
||||||
|
*/
|
||||||
|
export interface PingResponse {
|
||||||
|
/**
|
||||||
|
* Semantic version of the server.
|
||||||
|
*/
|
||||||
|
serverVersion: string;
|
||||||
|
/**
|
||||||
|
* Whether the client is authenticated based on the sent Authorization
|
||||||
|
* header.
|
||||||
|
*/
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface SerializedError {
|
||||||
|
errorType: string;
|
||||||
|
message: string;
|
||||||
|
causes: string[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface UpdateDocumentVersion {
|
||||||
|
parent_version_id: bigint;
|
||||||
|
relative_path: string;
|
||||||
|
content: number[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { CursorPositionFromClient } from "./CursorPositionFromClient";
|
||||||
|
import type { WebSocketHandshake } from "./WebSocketHandshake";
|
||||||
|
|
||||||
|
export type WebSocketClientMessage =
|
||||||
|
| ({ type: "handshake" } & WebSocketHandshake)
|
||||||
|
| ({ type: "cursorPositions" } & CursorPositionFromClient);
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export interface WebSocketHandshake {
|
||||||
|
token: string;
|
||||||
|
deviceId: string;
|
||||||
|
lastSeenVaultUpdateId: number | null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { CursorPositionFromServer } from "./CursorPositionFromServer";
|
||||||
|
import type { WebSocketVaultUpdate } from "./WebSocketVaultUpdate";
|
||||||
|
|
||||||
|
export type WebSocketServerMessage =
|
||||||
|
| ({ type: "vaultUpdate" } & WebSocketVaultUpdate)
|
||||||
|
| ({ type: "cursorPositions" } & CursorPositionFromServer);
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
|
||||||
|
|
||||||
|
export interface WebSocketVaultUpdate {
|
||||||
|
documents: DocumentVersionWithoutContent[];
|
||||||
|
isInitialSync: boolean;
|
||||||
|
}
|
||||||
209
frontend/sync-client/src/services/websocket-manager.ts
Normal file
209
frontend/sync-client/src/services/websocket-manager.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
import type { Database } from "../persistence/database";
|
||||||
|
import type { Logger } from "../tracing/logger";
|
||||||
|
import type { Settings, SyncSettings } from "../persistence/settings";
|
||||||
|
import type { WebSocketServerMessage } from "./types/WebSocketServerMessage";
|
||||||
|
import type { Syncer } from "../sync-operations/syncer";
|
||||||
|
import type { WebSocketClientMessage } from "./types/WebSocketClientMessage";
|
||||||
|
import type { CursorPositionFromClient } from "./types/CursorPositionFromClient";
|
||||||
|
import type { ClientCursors } from "./types/ClientCursors";
|
||||||
|
|
||||||
|
export class WebSocketManager {
|
||||||
|
private readonly webSocketStatusChangeListeners: (() => unknown)[] = [];
|
||||||
|
private readonly remoteCursorsUpdateListeners: ((
|
||||||
|
cursors: ClientCursors[]
|
||||||
|
) => unknown)[] = [];
|
||||||
|
|
||||||
|
private refreshWebSocketInterval: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
private webSocket: WebSocket | undefined;
|
||||||
|
|
||||||
|
private readonly webSocketFactoryImplementation: typeof globalThis.WebSocket;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly deviceId: string,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly database: Database,
|
||||||
|
private readonly settings: Settings,
|
||||||
|
private readonly syncer: Syncer,
|
||||||
|
webSocketImplementation?: typeof globalThis.WebSocket
|
||||||
|
) {
|
||||||
|
if (webSocketImplementation) {
|
||||||
|
this.webSocketFactoryImplementation = webSocketImplementation;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
typeof globalThis !== "undefined" &&
|
||||||
|
typeof globalThis.WebSocket === "undefined"
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.webSocketFactoryImplementation = require("ws"); // polyfill for WebSocket in Node.js
|
||||||
|
} else {
|
||||||
|
this.webSocketFactoryImplementation = WebSocket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateWebSocket(settings.getSettings());
|
||||||
|
|
||||||
|
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||||
|
if (
|
||||||
|
newSettings.remoteUri !== oldSettings.remoteUri ||
|
||||||
|
newSettings.vaultName !== oldSettings.vaultName ||
|
||||||
|
newSettings.token !== oldSettings.token ||
|
||||||
|
newSettings.isSyncEnabled !== oldSettings.isSyncEnabled
|
||||||
|
) {
|
||||||
|
this.updateWebSocket(newSettings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setWebSocketRefreshInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isWebSocketConnected(): boolean {
|
||||||
|
return (
|
||||||
|
this.webSocket?.readyState ===
|
||||||
|
this.webSocketFactoryImplementation.OPEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addWebSocketStatusChangeListener(listener: () => void): void {
|
||||||
|
this.webSocketStatusChangeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addRemoteCursorsUpdateListener(
|
||||||
|
listener: (cursors: ClientCursors[]) => void
|
||||||
|
): void {
|
||||||
|
this.remoteCursorsUpdateListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reset(): Promise<void> {
|
||||||
|
this.setWebSocketRefreshInterval();
|
||||||
|
this.updateWebSocket(this.settings.getSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
clearInterval(this.refreshWebSocketInterval);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.webSocket?.close();
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Failed to close WebSocket: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateLocalCursors(cursorPositions: CursorPositionFromClient): void {
|
||||||
|
if (!this.isWebSocketConnected) {
|
||||||
|
this.logger.warn(
|
||||||
|
"WebSocket is not connected, cannot send cursor positions"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message: WebSocketClientMessage = {
|
||||||
|
type: "cursorPositions",
|
||||||
|
...cursorPositions
|
||||||
|
};
|
||||||
|
this.webSocket?.send(JSON.stringify(message));
|
||||||
|
this.logger.info(
|
||||||
|
`Sent cursor positions: ${JSON.stringify(cursorPositions)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWebSocket(settings: SyncSettings): void {
|
||||||
|
try {
|
||||||
|
this.webSocket?.close();
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Failed to close WebSocket: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.isSyncEnabled) {
|
||||||
|
this.webSocket = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUri = new URL(settings.remoteUri);
|
||||||
|
wsUri.protocol = wsUri.protocol === "https" ? "wss" : "ws";
|
||||||
|
wsUri.pathname = `/vaults/${settings.vaultName}/ws`;
|
||||||
|
|
||||||
|
this.logger.info(`Connecting to WebSocket at ${wsUri.toString()}`);
|
||||||
|
|
||||||
|
this.webSocket = new this.webSocketFactoryImplementation(wsUri);
|
||||||
|
|
||||||
|
this.webSocket.onmessage = async (event): Promise<void> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
const message = JSON.parse(event.data) as WebSocketServerMessage;
|
||||||
|
|
||||||
|
if (message.type === "vaultUpdate") {
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
message.documents.map(async (document) =>
|
||||||
|
this.syncer.syncRemotelyUpdatedFile(document)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (message.isInitialSync && message.documents.length > 0) {
|
||||||
|
this.database.setLastSeenUpdateId(
|
||||||
|
message.documents
|
||||||
|
.map((document) => document.vaultUpdateId)
|
||||||
|
.reduce((a, b) => Math.max(a, b))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to sync remotely updated file: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
} else if (message.type === "cursorPositions") {
|
||||||
|
this.logger.info(
|
||||||
|
`Received cursor positions for ${JSON.stringify(message.clients)}`
|
||||||
|
);
|
||||||
|
this.remoteCursorsUpdateListeners.forEach((listener) => {
|
||||||
|
listener(
|
||||||
|
message.clients.filter(
|
||||||
|
(client) => client.deviceId !== this.deviceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`Received unknown message type: ${JSON.stringify(message)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The JS WebSocket API doesn't support setting headers, so we have to send the token as a message
|
||||||
|
this.webSocket.onopen = (): void => {
|
||||||
|
this.logger.info("WebSocket connection opened");
|
||||||
|
this.webSocketStatusChangeListeners.forEach((listener) => {
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
|
||||||
|
const message: WebSocketClientMessage = {
|
||||||
|
type: "handshake",
|
||||||
|
deviceId: this.deviceId,
|
||||||
|
token: settings.token,
|
||||||
|
lastSeenVaultUpdateId: this.database.getLastSeenUpdateId()
|
||||||
|
};
|
||||||
|
this.webSocket?.send(JSON.stringify(message));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.webSocket.onclose = (event): void => {
|
||||||
|
this.logger.warn(
|
||||||
|
`WebSocket closed with code ${event.code} (${event.reason == "" ? "unknown reason" : event.reason})`
|
||||||
|
);
|
||||||
|
this.webSocketStatusChangeListeners.forEach((listener) => {
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setWebSocketRefreshInterval(): void {
|
||||||
|
this.refreshWebSocketInterval = setInterval(() => {
|
||||||
|
if (
|
||||||
|
this.webSocket?.readyState ===
|
||||||
|
this.webSocketFactoryImplementation.CLOSED
|
||||||
|
) {
|
||||||
|
this.logger.info("WebSocket is closed, reconnecting...");
|
||||||
|
this.updateWebSocket(this.settings.getSettings());
|
||||||
|
}
|
||||||
|
}, this.settings.getSettings().webSocketRetryIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,9 +15,12 @@ import { FileOperations } from "./file-operations/file-operations";
|
||||||
import { ConnectionStatus } from "./services/connection-status";
|
import { ConnectionStatus } from "./services/connection-status";
|
||||||
import { UnrestrictedSyncer } from "./sync-operations/unrestricted-syncer";
|
import { UnrestrictedSyncer } from "./sync-operations/unrestricted-syncer";
|
||||||
import { rateLimit } from "./utils/rate-limit";
|
import { rateLimit } from "./utils/rate-limit";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import type { NetworkConnectionStatus } from "./types/network-connection-status";
|
import type { NetworkConnectionStatus } from "./types/network-connection-status";
|
||||||
import { DocumentUpdateStatus } from "./types/document-update-status";
|
import { DocumentUpdateStatus } from "./types/document-update-status";
|
||||||
|
import { WebSocketManager } from "./services/websocket-manager";
|
||||||
|
import { createClientId } from "./utils/create-client-id";
|
||||||
|
import type { CursorSpan } from "./services/types/CursorSpan";
|
||||||
|
import type { ClientCursors } from "./services/types/ClientCursors";
|
||||||
|
|
||||||
export class SyncClient {
|
export class SyncClient {
|
||||||
private static readonly MINIMUM_SAVE_INTERVAL_MS = 1000;
|
private static readonly MINIMUM_SAVE_INTERVAL_MS = 1000;
|
||||||
|
|
@ -29,6 +32,7 @@ export class SyncClient {
|
||||||
private readonly database: Database,
|
private readonly database: Database,
|
||||||
private readonly syncer: Syncer,
|
private readonly syncer: Syncer,
|
||||||
private readonly syncService: SyncService,
|
private readonly syncService: SyncService,
|
||||||
|
private readonly webSocketManager: WebSocketManager,
|
||||||
private readonly _logger: Logger,
|
private readonly _logger: Logger,
|
||||||
private readonly connectionStatus: ConnectionStatus
|
private readonly connectionStatus: ConnectionStatus
|
||||||
) {
|
) {
|
||||||
|
|
@ -68,7 +72,10 @@ export class SyncClient {
|
||||||
nativeLineEndings?: string;
|
nativeLineEndings?: string;
|
||||||
}): Promise<SyncClient> {
|
}): Promise<SyncClient> {
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
logger.info("Initialising SyncClient");
|
|
||||||
|
const deviceId = createClientId();
|
||||||
|
|
||||||
|
logger.info(`Initialising SyncClient with client id ${deviceId}`);
|
||||||
|
|
||||||
const history = new SyncHistory(logger);
|
const history = new SyncHistory(logger);
|
||||||
|
|
||||||
|
|
@ -104,7 +111,6 @@ export class SyncClient {
|
||||||
await rateLimitedSave(state);
|
await rateLimitedSave(state);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const deviceId = uuidv4();
|
|
||||||
|
|
||||||
const connectionStatus = new ConnectionStatus(settings, logger);
|
const connectionStatus = new ConnectionStatus(settings, logger);
|
||||||
const syncService = new SyncService(
|
const syncService = new SyncService(
|
||||||
|
|
@ -121,6 +127,7 @@ export class SyncClient {
|
||||||
fs,
|
fs,
|
||||||
nativeLineEndings
|
nativeLineEndings
|
||||||
);
|
);
|
||||||
|
|
||||||
const unrestrictedSyncer = new UnrestrictedSyncer(
|
const unrestrictedSyncer = new UnrestrictedSyncer(
|
||||||
logger,
|
logger,
|
||||||
database,
|
database,
|
||||||
|
|
@ -129,6 +136,7 @@ export class SyncClient {
|
||||||
fileOperations,
|
fileOperations,
|
||||||
history
|
history
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncer = new Syncer(
|
const syncer = new Syncer(
|
||||||
deviceId,
|
deviceId,
|
||||||
logger,
|
logger,
|
||||||
|
|
@ -136,7 +144,15 @@ export class SyncClient {
|
||||||
settings,
|
settings,
|
||||||
syncService,
|
syncService,
|
||||||
fileOperations,
|
fileOperations,
|
||||||
unrestrictedSyncer,
|
unrestrictedSyncer
|
||||||
|
);
|
||||||
|
|
||||||
|
const webSocketManager = new WebSocketManager(
|
||||||
|
deviceId,
|
||||||
|
logger,
|
||||||
|
database,
|
||||||
|
settings,
|
||||||
|
syncer,
|
||||||
webSocket
|
webSocket
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -146,6 +162,7 @@ export class SyncClient {
|
||||||
database,
|
database,
|
||||||
syncer,
|
syncer,
|
||||||
syncService,
|
syncService,
|
||||||
|
webSocketManager,
|
||||||
logger,
|
logger,
|
||||||
connectionStatus
|
connectionStatus
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +177,7 @@ export class SyncClient {
|
||||||
return {
|
return {
|
||||||
isSuccessful: server.isSuccessful,
|
isSuccessful: server.isSuccessful,
|
||||||
serverMessage: server.message,
|
serverMessage: server.message,
|
||||||
isWebSocketConnected: this.syncer.isWebSocketConnected
|
isWebSocketConnected: this.webSocketManager.isWebSocketConnected
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +196,7 @@ export class SyncClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
this.syncer.stop();
|
this.webSocketManager.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitAndStop(): Promise<void> {
|
public async waitAndStop(): Promise<void> {
|
||||||
|
|
@ -194,6 +211,7 @@ export class SyncClient {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.connectionStatus.startReset();
|
this.connectionStatus.startReset();
|
||||||
await this.syncer.reset();
|
await this.syncer.reset();
|
||||||
|
await this.webSocketManager.reset();
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this.database.reset();
|
this.database.reset();
|
||||||
this._logger.reset();
|
this._logger.reset();
|
||||||
|
|
@ -229,7 +247,7 @@ export class SyncClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public addWebSocketStatusChangeListener(listener: () => void): void {
|
public addWebSocketStatusChangeListener(listener: () => void): void {
|
||||||
this.syncer.addWebSocketStatusChangeListener(listener);
|
this.webSocketManager.addWebSocketStatusChangeListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async syncLocallyCreatedFile(
|
public async syncLocallyCreatedFile(
|
||||||
|
|
@ -257,6 +275,18 @@ export class SyncClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateLocalCursors(
|
||||||
|
documentToCursors: Record<RelativePath, CursorSpan[]>
|
||||||
|
): Promise<void> {
|
||||||
|
this.webSocketManager.updateLocalCursors({ documentToCursors });
|
||||||
|
}
|
||||||
|
|
||||||
|
public addRemoteCursorsUpdateListener(
|
||||||
|
listener: (cursors: ClientCursors[]) => void
|
||||||
|
): void {
|
||||||
|
this.webSocketManager.addRemoteCursorsUpdateListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
public getDocumentSyncingStatus(
|
public getDocumentSyncingStatus(
|
||||||
relativePath: RelativePath
|
relativePath: RelativePath
|
||||||
): DocumentUpdateStatus {
|
): DocumentUpdateStatus {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import type { Logger } from "../tracing/logger";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import { hash } from "../utils/hash";
|
import { hash } from "../utils/hash";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import type { components } from "../services/types";
|
|
||||||
import type { Settings, SyncSettings } from "../persistence/settings";
|
import type { Settings, SyncSettings } from "../persistence/settings";
|
||||||
import type { FileOperations } from "../file-operations/file-operations";
|
import type { FileOperations } from "../file-operations/file-operations";
|
||||||
import { findMatchingFile } from "../utils/find-matching-file";
|
import { findMatchingFile } from "../utils/find-matching-file";
|
||||||
|
|
@ -17,27 +16,16 @@ import type { UnrestrictedSyncer } from "./unrestricted-syncer";
|
||||||
import { createPromise } from "../utils/create-promise";
|
import { createPromise } from "../utils/create-promise";
|
||||||
import { SyncResetError } from "../services/sync-reset-error";
|
import { SyncResetError } from "../services/sync-reset-error";
|
||||||
import { Locks } from "../utils/locks";
|
import { Locks } from "../utils/locks";
|
||||||
|
import type { DocumentVersionWithoutContent } from "../services/types/DocumentVersionWithoutContent";
|
||||||
interface WebsocketVaultUpdate {
|
|
||||||
documents: components["schemas"]["DocumentVersionWithoutContent"][];
|
|
||||||
isInitialSync: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Syncer {
|
export class Syncer {
|
||||||
private readonly remoteDocumentsLock: Locks<DocumentId>;
|
private readonly remoteDocumentsLock: Locks<DocumentId>;
|
||||||
private readonly remainingOperationsListeners: ((
|
private readonly remainingOperationsListeners: ((
|
||||||
remainingOperations: number
|
remainingOperations: number
|
||||||
) => void)[] = [];
|
) => void)[] = [];
|
||||||
private readonly webSocketStatusChangeListeners: (() => void)[] = [];
|
|
||||||
private readonly syncQueue: PQueue;
|
private readonly syncQueue: PQueue;
|
||||||
|
|
||||||
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
|
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
|
||||||
private refreshApplyRemoteChangesWebSocketInterval:
|
|
||||||
| NodeJS.Timeout
|
|
||||||
| undefined;
|
|
||||||
private applyRemoteChangesWebSocket: WebSocket | undefined;
|
|
||||||
|
|
||||||
private readonly webSocketImplementation: typeof globalThis.WebSocket;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/max-params
|
// eslint-disable-next-line @typescript-eslint/max-params
|
||||||
public constructor(
|
public constructor(
|
||||||
|
|
@ -47,41 +35,15 @@ export class Syncer {
|
||||||
private readonly settings: Settings,
|
private readonly settings: Settings,
|
||||||
private readonly syncService: SyncService,
|
private readonly syncService: SyncService,
|
||||||
private readonly operations: FileOperations,
|
private readonly operations: FileOperations,
|
||||||
private readonly internalSyncer: UnrestrictedSyncer,
|
private readonly internalSyncer: UnrestrictedSyncer
|
||||||
webSocketImplementation?: typeof globalThis.WebSocket
|
|
||||||
) {
|
) {
|
||||||
this.syncQueue = new PQueue({
|
this.syncQueue = new PQueue({
|
||||||
concurrency: settings.getSettings().syncConcurrency
|
concurrency: settings.getSettings().syncConcurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
if (webSocketImplementation) {
|
|
||||||
this.webSocketImplementation = webSocketImplementation;
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
typeof globalThis !== "undefined" &&
|
|
||||||
typeof globalThis.WebSocket === "undefined"
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
this.webSocketImplementation = require("ws"); // polyfill for WebSocket in Node.js
|
|
||||||
} else {
|
|
||||||
this.webSocketImplementation = WebSocket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateWebSocket(settings.getSettings());
|
|
||||||
|
|
||||||
this.remoteDocumentsLock = new Locks<DocumentId>(this.logger);
|
this.remoteDocumentsLock = new Locks<DocumentId>(this.logger);
|
||||||
|
|
||||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||||
if (
|
|
||||||
newSettings.remoteUri !== oldSettings.remoteUri ||
|
|
||||||
newSettings.vaultName !== oldSettings.vaultName ||
|
|
||||||
newSettings.token !== oldSettings.token ||
|
|
||||||
newSettings.isSyncEnabled !== oldSettings.isSyncEnabled
|
|
||||||
) {
|
|
||||||
this.updateWebSocket(newSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSettings.syncConcurrency !== oldSettings.syncConcurrency) {
|
if (newSettings.syncConcurrency !== oldSettings.syncConcurrency) {
|
||||||
this.syncQueue.concurrency = newSettings.syncConcurrency;
|
this.syncQueue.concurrency = newSettings.syncConcurrency;
|
||||||
}
|
}
|
||||||
|
|
@ -92,15 +54,6 @@ export class Syncer {
|
||||||
listener(this.syncQueue.size);
|
listener(this.syncQueue.size);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setWebSocketRefreshInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isWebSocketConnected(): boolean {
|
|
||||||
return (
|
|
||||||
this.applyRemoteChangesWebSocket?.readyState ===
|
|
||||||
this.webSocketImplementation.OPEN
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRemainingOperationsListener(
|
public addRemainingOperationsListener(
|
||||||
|
|
@ -109,10 +62,6 @@ export class Syncer {
|
||||||
this.remainingOperationsListeners.push(listener);
|
this.remainingOperationsListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addWebSocketStatusChangeListener(listener: () => void): void {
|
|
||||||
this.webSocketStatusChangeListeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async syncLocallyCreatedFile(
|
public async syncLocallyCreatedFile(
|
||||||
relativePath: RelativePath
|
relativePath: RelativePath
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -303,106 +252,10 @@ export class Syncer {
|
||||||
|
|
||||||
public async reset(): Promise<void> {
|
public async reset(): Promise<void> {
|
||||||
await this.waitUntilFinished();
|
await this.waitUntilFinished();
|
||||||
this.setWebSocketRefreshInterval();
|
|
||||||
this.updateWebSocket(this.settings.getSettings());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public async syncRemotelyUpdatedFile(
|
||||||
clearInterval(this.refreshApplyRemoteChangesWebSocketInterval);
|
remoteVersion: DocumentVersionWithoutContent
|
||||||
|
|
||||||
try {
|
|
||||||
this.applyRemoteChangesWebSocket?.close();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn(`Failed to close WebSocket: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateWebSocket(settings: SyncSettings): void {
|
|
||||||
try {
|
|
||||||
this.applyRemoteChangesWebSocket?.close();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn(`Failed to close WebSocket: ${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settings.isSyncEnabled) {
|
|
||||||
this.applyRemoteChangesWebSocket = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsUri = new URL(settings.remoteUri);
|
|
||||||
wsUri.protocol = wsUri.protocol === "https" ? "wss" : "ws";
|
|
||||||
wsUri.pathname = `/vaults/${settings.vaultName}/ws`;
|
|
||||||
|
|
||||||
this.logger.info(`Connecting to WebSocket at ${wsUri.toString()}`);
|
|
||||||
|
|
||||||
this.applyRemoteChangesWebSocket = new this.webSocketImplementation(
|
|
||||||
wsUri
|
|
||||||
);
|
|
||||||
|
|
||||||
this.applyRemoteChangesWebSocket.onmessage = async (
|
|
||||||
event
|
|
||||||
): Promise<void> => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
||||||
const message = JSON.parse(event.data) as WebsocketVaultUpdate;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
message.documents.map(async (document) =>
|
|
||||||
this.syncRemotelyUpdatedFile(document)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (message.isInitialSync && message.documents.length > 0) {
|
|
||||||
this.database.setLastSeenUpdateId(
|
|
||||||
message.documents
|
|
||||||
.map((document) => document.vaultUpdateId)
|
|
||||||
.reduce((a, b) => Math.max(a, b))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error(`Failed to sync remotely updated file: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The JS WebSocket API doesn't support setting headers, so we have to send the token as a message
|
|
||||||
this.applyRemoteChangesWebSocket.onopen = (): void => {
|
|
||||||
this.logger.info("WebSocket connection opened");
|
|
||||||
this.applyRemoteChangesWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
deviceId: this.deviceId,
|
|
||||||
token: settings.token,
|
|
||||||
lastSeenVaultUpdateId: this.database.getLastSeenUpdateId()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.webSocketStatusChangeListeners.forEach((listener) => {
|
|
||||||
listener();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyRemoteChangesWebSocket.onclose = (event): void => {
|
|
||||||
this.logger.warn(
|
|
||||||
`WebSocket closed with code ${event.code} (${event.reason == "" ? "unknown reason" : event.reason})`
|
|
||||||
);
|
|
||||||
this.webSocketStatusChangeListeners.forEach((listener) => {
|
|
||||||
listener();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private setWebSocketRefreshInterval(): void {
|
|
||||||
this.refreshApplyRemoteChangesWebSocketInterval = setInterval(() => {
|
|
||||||
if (
|
|
||||||
this.applyRemoteChangesWebSocket?.readyState ===
|
|
||||||
this.webSocketImplementation.OPEN
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateWebSocket(this.settings.getSettings());
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async syncRemotelyUpdatedFile(
|
|
||||||
remoteVersion: components["schemas"]["DocumentVersionWithoutContent"]
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let document = this.database.getDocumentByDocumentId(
|
let document = this.database.getDocumentByDocumentId(
|
||||||
remoteVersion.documentId
|
remoteVersion.documentId
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import type {
|
||||||
} from "../tracing/sync-history";
|
} from "../tracing/sync-history";
|
||||||
import { SyncStatus, SyncType } from "../tracing/sync-history";
|
import { SyncStatus, SyncType } from "../tracing/sync-history";
|
||||||
import { EMPTY_HASH, hash } from "../utils/hash";
|
import { EMPTY_HASH, hash } from "../utils/hash";
|
||||||
import type { components } from "../services/types";
|
|
||||||
import { deserialize } from "../utils/deserialize";
|
import { deserialize } from "../utils/deserialize";
|
||||||
import type { Settings } from "../persistence/settings";
|
import type { Settings } from "../persistence/settings";
|
||||||
import type { FileOperations } from "../file-operations/file-operations";
|
import type { FileOperations } from "../file-operations/file-operations";
|
||||||
|
|
@ -25,6 +24,9 @@ import { createPromise } from "../utils/create-promise";
|
||||||
import { FileNotFoundError } from "../file-operations/file-not-found-error";
|
import { FileNotFoundError } from "../file-operations/file-not-found-error";
|
||||||
import { SyncResetError } from "../services/sync-reset-error";
|
import { SyncResetError } from "../services/sync-reset-error";
|
||||||
import { globsToRegexes } from "../utils/globs-to-regexes";
|
import { globsToRegexes } from "../utils/globs-to-regexes";
|
||||||
|
import type { DocumentVersion } from "../services/types/DocumentVersion";
|
||||||
|
import type { DocumentUpdateResponse } from "../services/types/DocumentUpdateResponse";
|
||||||
|
import type { DocumentVersionWithoutContent } from "../services/types/DocumentVersionWithoutContent";
|
||||||
|
|
||||||
export class UnrestrictedSyncer {
|
export class UnrestrictedSyncer {
|
||||||
private ignorePatterns: RegExp[];
|
private ignorePatterns: RegExp[];
|
||||||
|
|
@ -172,10 +174,8 @@ export class UnrestrictedSyncer {
|
||||||
document.metadata.hash === contentHash && oldPath === undefined
|
document.metadata.hash === contentHash && oldPath === undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
let response:
|
let response: DocumentVersion | DocumentUpdateResponse | undefined =
|
||||||
| components["schemas"]["DocumentVersion"]
|
undefined;
|
||||||
| components["schemas"]["DocumentUpdateResponse"]
|
|
||||||
| undefined = undefined;
|
|
||||||
|
|
||||||
if (areThereLocalChanges) {
|
if (areThereLocalChanges) {
|
||||||
response = await this.syncService.put({
|
response = await this.syncService.put({
|
||||||
|
|
@ -332,7 +332,7 @@ export class UnrestrictedSyncer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unrestrictedSyncRemotelyUpdatedFile(
|
public async unrestrictedSyncRemotelyUpdatedFile(
|
||||||
remoteVersion: components["schemas"]["DocumentVersionWithoutContent"],
|
remoteVersion: DocumentVersionWithoutContent,
|
||||||
document?: DocumentRecord
|
document?: DocumentRecord
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const updateDetails: SyncCreateDetails = {
|
const updateDetails: SyncCreateDetails = {
|
||||||
|
|
|
||||||
15
frontend/sync-client/src/utils/create-client-id.ts
Normal file
15
frontend/sync-client/src/utils/create-client-id.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export function createClientId(): string {
|
||||||
|
// @ts-expect-error, injected by webpack
|
||||||
|
const packageVersion = __CURRENT_VERSION__; // eslint-disable-line
|
||||||
|
|
||||||
|
const platform =
|
||||||
|
typeof navigator !== "undefined"
|
||||||
|
? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated
|
||||||
|
: typeof process !== "undefined"
|
||||||
|
? process.platform
|
||||||
|
: "unknown";
|
||||||
|
|
||||||
|
return `vault-link/${packageVersion} (${uuidv4()}; ${platform})`;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* A type-safe utility function to create a Promise with resolve and reject functions.
|
||||||
|
* @returns A tuple containing a Promise, a resolve function, and a reject function.
|
||||||
|
*/
|
||||||
export function createPromise<T = void>(): [
|
export function createPromise<T = void>(): [
|
||||||
Promise<T>,
|
Promise<T>,
|
||||||
(value: T) => void,
|
(value: T) => void,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { Logger } from "../tracing/logger";
|
||||||
// Locks are granted in a first-in-first-out order.
|
// Locks are granted in a first-in-first-out order.
|
||||||
export class Locks<T> {
|
export class Locks<T> {
|
||||||
private readonly locked = new Set<T>();
|
private readonly locked = new Set<T>();
|
||||||
private readonly waiters = new Map<T, (() => void)[]>();
|
private readonly waiters = new Map<T, (() => unknown)[]>();
|
||||||
|
|
||||||
public constructor(private readonly logger: Logger) {}
|
public constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.27",
|
"@types/node": "^22.15.30",
|
||||||
"sync-client": "file:../sync-client",
|
"sync-client": "file:../sync-client",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"bufferutil": "^4.0.9"
|
"bufferutil": "^4.0.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
./scripts/utils/wait-for-server.sh
|
rm -rf backend/sync_server/bindings
|
||||||
|
|
||||||
npm install -g openapi-typescript
|
cd backend
|
||||||
openapi-typescript http://localhost:3000/api.json --output frontend/sync-client/src/services/types.ts
|
cargo test export_bindings
|
||||||
|
cd -
|
||||||
|
|
||||||
|
cp -r backend/sync_server/bindings/* frontend/sync-client/src/services/types/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue