Merge pull request #4 from schmelczer/asch/better-api

Improve public API & add docs
This commit is contained in:
Andras Schmelczer 2025-07-06 13:07:51 +01:00 • committed by GitHub
commit f0a204b092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 6832 additions and 779 deletions

View file

@ -14,3 +14,8 @@ updates:
directories: ["**"] directories: ["**"]
schedule: schedule:
interval: "daily" interval: "daily"
- package-ecosystem: "npm"
directories: ["/reconcile-js"]
schedule:
interval: "daily"

View file

@ -40,4 +40,6 @@ jobs:
- name: Test - name: Test
run: | run: |
cargo test --verbose -- --include-ignored cargo test --verbose -- --include-ignored
cargo test --features serde
cargo test --features wasm
wasm-pack test --node --features wasm wasm-pack test --node --features wasm

6
.gitignore vendored
View file

@ -3,3 +3,9 @@
# Rust build folder # Rust build folder
target target
# Node dependencies
node_modules
# WebPack build output
dist

13
.vscode/settings.json vendored
View file

@ -1,7 +1,12 @@
{ {
"files.exclude": { "files.exclude": {
"**/dist": true, "**/snapshots": true, // cargo-insta outputs
"**/node_modules": true, "**/node_modules": true, // node.js dependencies
"**/snapshots": true, "**/dist": true, // webpack build directory
} "pkg": true, // wasm-pack build directory
"target": true, // rust build directory
},
"rust-analyzer.cargo.features": [
"all"
]
} }

230
Cargo.lock generated
View file

@ -4,35 +4,41 @@ version = 4
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.2" version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.8" version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [ dependencies = [
"encode_unicode", "encode_unicode",
"lazy_static",
"libc", "libc",
"windows-sys 0.52.0", "once_cell",
"windows-sys",
] ]
[[package]] [[package]]
@ -41,7 +47,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.1",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -53,27 +59,27 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "0.3.6" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.7.0" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -81,50 +87,36 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.42.2" version = "1.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
dependencies = [ dependencies = [
"console", "console",
"linked-hash-map",
"once_cell", "once_cell",
"pin-project",
"similar", "similar",
] ]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.76" version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.171" version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "log" name = "log"
@ -132,6 +124,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]] [[package]]
name = "minicov" name = "minicov"
version = "0.3.7" version = "0.3.7"
@ -144,29 +142,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.2" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
@ -180,18 +158,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -200,6 +178,7 @@ dependencies = [
name = "reconcile" name = "reconcile"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"cfg-if 1.0.1",
"console_error_panic_hook", "console_error_panic_hook",
"insta", "insta",
"pretty_assertions", "pretty_assertions",
@ -208,13 +187,20 @@ dependencies = [
"test-case", "test-case",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-test", "wasm-bindgen-test",
"wee_alloc",
] ]
[[package]] [[package]]
name = "ryu" name = "rustversion"
version = "1.0.18" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "same-file" name = "same-file"
@ -225,12 +211,6 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@ -272,15 +252,15 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.90" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -302,7 +282,7 @@ version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -322,9 +302,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
@ -344,20 +324,21 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.1",
"once_cell", "once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@ -369,11 +350,11 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.49" version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.1",
"js-sys", "js-sys",
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@ -382,9 +363,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -392,9 +373,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -405,19 +386,21 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d44563646eb934577f2772656c7ad5e9c90fac78aa8013d776fcdaf24625d" checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"minicov", "minicov",
"scoped-tls",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-bindgen-test-macro", "wasm-bindgen-test-macro",
@ -425,9 +408,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-test-macro" name = "wasm-bindgen-test-macro"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54171416ce73aa0b9c377b51cc3cb542becee1cd678204812e8392e5b0e4a031" checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -436,31 +419,56 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.76" version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.9" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys",
] ]
[[package]] [[package]]
name = "windows-sys" name = "winapi-x86_64-pc-windows-gnu"
version = "0.52.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"

View file

@ -21,6 +21,9 @@ wasm-bindgen = { version = "0.2.99", optional = true }
# code size when deploying. # code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true } console_error_panic_hook = { version = "0.1.7", optional = true }
wee_alloc = { version = "0.4.2", optional = true }
cfg-if = "1.0.1"
[features] [features]
default = [] default = []
serde = [ "dep:serde" ] serde = [ "dep:serde" ]
@ -39,7 +42,10 @@ wasm-bindgen-test = "0.3.49"
codegen-units = 1 codegen-units = 1
lto = true lto = true
opt-level = 3 opt-level = 3
strip="debuginfo" # Keep some info for better panics strip="symbols"
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-O4', '--enable-bulk-memory']
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
@ -76,11 +82,6 @@ verbose_file_reads = "warn"
large_stack_arrays = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/13774 large_stack_arrays = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/13774
# TODO: fix these
cast_possible_truncation = { level = "allow", priority = 1 }
cast_sign_loss = { level = "allow", priority = 1 }
cast_possible_wrap = { level = "allow", priority = 1 }
# Silly lints # Silly lints
implicit_return = { level = "allow", priority = 1 } implicit_return = { level = "allow", priority = 1 }
question_mark_used = { level = "allow", priority = 1 } question_mark_used = { level = "allow", priority = 1 }

170
README.md
View file

@ -1,54 +1,150 @@
# VaultLink self-hosted Obsidian plugin for file syncing # Reconcile: conflict-free 3-way text merging
[![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) [![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml)
[![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) [![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml)
## Develop > [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) (or `git merge`) but with automatic conflict resolution.
### Install [nvm](https://github.com/nvm-sh/nvm) Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. It automatically resolves conflicts that would typically require user action in traditional 3-way merge tools.
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` Try out the [interactive demo](https://schmelczer.dev/reconcile)!
TODO: add links for crates and npm
## Features
- **Conflict-free output** - No more git conflict markers in the result
- **Cursor/selection position tracking** - Automatically updates cursor positions during merging
- **Pluggable tokenizer** - Choose between word-level, character-level, or custom tokenization
- **Full UTF-8 support** - Handles Unicode text correctly
- **WebAssembly support** - Use from JavaScript/TypeScript applications
## Quick Start
### Rust
Add to your `Cargo.toml`:
```toml
[dependencies]
reconcile = "0.4"
```
```rust
use reconcile::{reconcile, BuiltinTokenizer};
let parent = "Hello world";
let left = "Hello beautiful world";
let right = "Hi world";
let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
assert_eq!(result.apply().text(), "Hi beautiful world");
```
### JavaScript/TypeScript
```bash
npm install reconcile
```
```javascript
import { init, reconcile } from "reconcile";
// Initialize the WASM module (required before first use)
await init();
const parent = "Hello world";
const left = "Hello beautiful world";
const right = "Hi world";
const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
```
## API
### Tokenizers
Reconcile supports different tokenization strategies:
- **Word tokenizer** (`BuiltinTokenizer::Word`): Splits text into words (default, recommended for most use cases)
- **Character tokenizer** (`BuiltinTokenizer::Character`): Splits text into individual characters (fine-grained merging)
- **Custom tokenizer**: Implement your own tokenization logic
### Cursor Tracking
Reconcile can automatically update cursor and selection positions during merging:
```javascript
const result = reconcile(
"Hello world",
{
text: "Hello beautiful world",
cursors: [{ id: 1, position: 6 }], // After "Hello "
},
{
text: "Hi world",
cursors: [{ id: 2, position: 0 }], // At beginning
}
);
// Result includes updated cursor positions
console.log(result.cursors); // [{ id: 1, position: 3 }, { id: 2, position: 0 }]
```
### History Tracking
Use `reconcileWithHistory` to get detailed information about the merge process:
```javascript
const result = reconcileWithHistory(parent, left, right);
console.log(result.history); // Array of spans with their origins
```
## Algorithm
The algorithm starts similarly to `diff3`. Its inputs are a **parent** document and two conflicting versions: `left` and `right` which have been created from the parent through any series of concurrent edits.
1. **Diff calculation**: First, 2-way diffs of (parent & left) and (parent & right) are computed using Myers' algorithm
2. **Tokenization**: The text is split into tokens (words, characters, etc.) for granular merging
3. **Diff cleaning**: The tokens of the same diff are reordered and merged to end up to maximise patch sizes
4. **Operation transformation (OT)**: The resulting edits are weaved together using operational transformation principles, ensuring no changes are lost
`EditedText` (at least in the Rust library) exposes an implementation of OT. The primary purpose of this library isn't to implement OT but to provide automated text merging, howver, OT happens to provide an easy way of merging the output of Myers' diff. The same result could be achieved through many CRDT implementations as well. However, the merging quality is only as good as the 2-way diffs are. For instance, `reconcile` doesn't support `move` semantics as these are decomposed into an `insert` and `delete` operation by Myers'.
## Motivation
Sometimes documents get edited concurrently by multiple users (or the same user from multiple devices) resulting in divergent changes.
To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some applications, this is trivial if the document can only be edited through an editor that's in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder of Markdown files leaving the user free to change their technology of choice on a whim.
This means that files can be edited out-of-channel and the only information a text synchronization system can know is the current content of each tracked file. This is described as Differential Synchronization [1]. This is the same problem as what Git and similar version control systems solve but in a manual way. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary.
> There are domains of human text which are less tolerant of mis-merges: for instance, two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus unknowingly changing the meaning.
## Development
### Prerequisites
#### Install Node.js
- Install [nvm](https://github.com/nvm-sh/nvm): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash`
- `nvm install 22` - `nvm install 22`
- `nvm use 22` - `nvm use 22`
- Optionally set the system-wide default: `nvm alias default 22` - Optionally set the system-wide default: `nvm alias default 22`
### Set up Rust #### Set up Rust
- Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` - Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
- Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer): `curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh` - `cargo install wasm-pack cargo-insta cargo-edit`
- `cargo install cargo-insta sqlx-cli cargo-edit`
### Install Obsidian on Linux
```sh
apt install flatpak
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install flathub md.obsidian.Obsidian
flatpak run md.obsidian.Obsidian
```
### Scripts ### Scripts
#### Update HTTP API TS bindings - **Running tests**: `scripts/test.sh`
- **Formatting**: `scripts/lint.sh`
- **Building website**: `scripts/dev-website.sh`
- **Publishing new version**: `scripts/bump-version.sh patch`
```sh TODO: license
scripts/update-api-types.sh
```
#### Publish new version [1]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf
```sh
scripts/bump-version.sh patch
```
#### Run E2E tests
```sh
scripts/e2e.sh
```
And to clean up the logs & database files, run `scripts/clean-up.sh`
## Projects
- [Sync server](./backend/sync_server/README.md)

View file

@ -1,3 +0,0 @@
reconcile.js
reconcile_bg.wasm

View file

@ -1,4 +1,4 @@
import init, { mergeTextWithHistory } from "./reconcile.js"; import { init, reconcileWithHistory } from "./dist/index.js";
const originalTextArea = document.getElementById("original"); const originalTextArea = document.getElementById("original");
const leftTextArea = document.getElementById("left"); const leftTextArea = document.getElementById("left");
@ -37,16 +37,15 @@ function updateMergedText() {
const left = leftTextArea.value; const left = leftTextArea.value;
const right = rightTextArea.value; const right = rightTextArea.value;
const results = mergeTextWithHistory(original, left, right); const results = reconcileWithHistory(original, left, right);
mergedTextArea.innerHTML = ""; mergedTextArea.innerHTML = "";
for (const result of results) { for (const {text, history} of results.history) {
const span = document.createElement("span"); const span = document.createElement("span");
span.className = result.history(); span.className = history;
span.textContent = result.text(); span.textContent = text;
mergedTextArea.appendChild(span); mergedTextArea.appendChild(span);
result.free();
} }
} }

View file

@ -0,0 +1,3 @@
module.exports = {
preset: "ts-jest/presets/js-with-babel-esm"
};

4905
reconcile-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
reconcile-js/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "reconcile",
"version": "0.4.0",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/*"
],
"scripts": {
"build": "webpack --mode production",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"reconcile": "file:../pkg",
"ts-jest": "^29.3.4",
"ts-loader": "^9.5.2",
"tslib": "2.8.1",
"typescript": "5.8.3",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}
}

View file

@ -0,0 +1,70 @@
import { init, reconcile, reconcileWithHistory } from "./index";
import * as fs from "fs";
describe("reconcile", () => {
it("tries calling functions without init", () => {
expect(() => reconcile("Hello", "Hello world", "Hi world")).toThrow(
/call init()/
);
expect(() =>
reconcileWithHistory("Hello", "Hello world", "Hi world")
).toThrow(/call init()/);
});
it("call reconcile without cursors", async () => {
await initWasm();
expect(reconcile("Hello", "Hello world", "Hi world").text).toEqual(
"Hi world"
);
});
it("call reconcile with cursors", async () => {
await initWasm();
const result = reconcile(
"Hello",
{
text: "Hello world",
cursors: [
{
id: 3,
position: 2,
},
],
},
{
text: "Hi world",
cursors: [
{
id: 4,
position: 0,
},
{ id: 5, position: 3 },
],
}
);
expect(result.text).toEqual("Hi world");
expect(result.cursors).toEqual([
{ id: 3, position: 0 },
{ id: 4, position: 0 },
{ id: 5, position: 3 },
]);
});
it("call reconcileWithHistory", async () => {
await initWasm();
const result = reconcileWithHistory("Hello", "Hello world", "Hi world");
expect(result.text).toEqual("Hi world");
expect(result.history.length).toBeGreaterThan(0);
});
});
async function initWasm() {
const wasmBin = fs.readFileSync("../pkg/reconcile_bg.wasm");
await init({ module_or_path: wasmBin });
}

201
reconcile-js/src/index.ts Normal file
View file

@ -0,0 +1,201 @@
import wasmInit, {
CursorPosition as wasmCursorPosition,
reconcile as wasmReconcile,
TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory,
BuiltinTokenizer,
reconcileWithHistory as wasmReconcileWithHistory,
History,
InitInput,
} from "reconcile";
export interface TextWithCursors {
/** The document's entire content */
text: string;
/** List of cursor positions, can be null or undefined if there are no cursors */
cursors: null | undefined | CursorPosition[];
}
export interface CursorPosition {
/** Unique identifier for the cursor */
id: number;
/** Character position in the text, 0-based */
position: number;
}
export interface TextWithCursorsAndHistory {
/** The document's entire content */
text: string;
/** List of cursor positions, can be null or undefined if there are no cursors */
cursors: null | undefined | CursorPosition[];
/** List of operations leading to `text` from the 3 ancestors */
history: SpanWithHistory[];
}
export interface SpanWithHistory {
/** Span of text associated with the historical opearion */
text: string;
/** Origin of the `text` span */
history: History;
}
export type Tokenizer = "Line" | "Word" | "Character";
const TOKENIZERS = ["Line", "Word", "Character"];
let isInitialised = false;
const UNINITIALISED_MODULE_ERROR =
"Reconcile module has not been initialized. Please call init() before using any other functions.";
const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${TOKENIZERS.join(
", "
)} are supported.`;
/**
* Initializes the WASM module for text reconciliation.
* Must be called before using any other functions.
*
* The function is idempotent.
*
* @param content - Optional initialization input for the WASM module during testing.
* @returns Promise that resolves when initialization is complete
*/
export async function init(content?: InitInput) {
if (isInitialised) {
return;
}
await wasmInit(content);
isInitialised = true;
}
/**
* Merges three versions of text (original, left, right) and cursor positions.
*
* @param original - The original/base version of the text
* @param left - The left version of the text, either as string or TextWithCursors
* @param right - The right version of the text, either as string or TextWithCursors
* @param tokenizer - The tokenization strategy to use (default: "Word")
* @returns The reconciled text with merged cursor positions
*/
export function reconcile(
original: string,
left: string | TextWithCursors,
right: string | TextWithCursors,
tokenizer: BuiltinTokenizer = "Word"
): TextWithCursors {
if (!isInitialised) {
throw new Error(UNINITIALISED_MODULE_ERROR);
}
if (!TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
const leftCursor = toWasmTextWithCursors(left);
const rightCursor = toWasmTextWithCursors(right);
const result = wasmReconcile(original, leftCursor, rightCursor, tokenizer);
leftCursor.free();
rightCursor.free();
const jsResult = toTextWithCursors(result);
result.free();
return jsResult;
}
/**
* Merges three versions of text and returns the result with historical information.
*
* Calculating the `history` is somewhat more expensive, otherwise this function behaves like `reconcile`.
*
* @param original - The original/base version of the text
* @param left - The left version of the text, either as string or TextWithCursors
* @param right - The right version of the text, either as string or TextWithCursors
* @param tokenizer - The tokenization strategy to use (default: "Word")
* @returns The reconciled text with cursor positions and history of changes
*/
export function reconcileWithHistory(
original: string,
left: string | TextWithCursors,
right: string | TextWithCursors,
tokenizer: BuiltinTokenizer = "Word"
): TextWithCursorsAndHistory {
if (!isInitialised) {
throw new Error(UNINITIALISED_MODULE_ERROR);
}
if (!TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
const leftCursor = toWasmTextWithCursors(left);
const rightCursor = toWasmTextWithCursors(right);
const result = wasmReconcileWithHistory(
original,
leftCursor,
rightCursor,
tokenizer
);
leftCursor.free();
rightCursor.free();
const jsResult = toTextWithCursors(result);
const history = result.history().map(toSpanWithHistory);
result.free();
return {
...jsResult,
history,
};
}
function toWasmTextWithCursors(
text: string | TextWithCursors
): wasmTextWithCursors {
const isInputString = typeof text == "string";
const leftText = isInputString ? text : text.text;
const leftCursors = isInputString ? [] : text.cursors ?? [];
return new wasmTextWithCursors(
leftText,
leftCursors.map(toWasmCursorPosition)
);
}
function toWasmCursorPosition({
id,
position,
}: CursorPosition): wasmCursorPosition {
return new wasmCursorPosition(id, position);
}
function toTextWithCursors(
textWithCursor: wasmTextWithCursors
): TextWithCursors {
return {
text: textWithCursor.text(),
cursors: textWithCursor.cursors().map(toCursorPosition),
};
}
function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
return {
id: cursor.id(),
position: cursor.characterIndex(),
};
}
function toSpanWithHistory(
textWithHistory: wasmSpanWithHistory
): SpanWithHistory {
return {
text: textWithHistory.text(),
history: textWithHistory.history(),
};
}

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"declaration": true,
"declarationDir": "./dist/types",
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"inlineSourceMap": true
},
"exclude": [
"./dist"
]
}

View file

@ -0,0 +1,37 @@
const path = require("path");
module.exports = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.ts$/,
use: ["ts-loader"]
},
{
test: /\.wasm$/,
type: "asset/inline"
}
]
},
optimization: {
// the consuming project should take care of minification
minimize: false
},
resolve: {
extensions: [".ts"],
alias: {
root: __dirname,
src: path.resolve(__dirname, "src")
}
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
libraryTarget: "module"
},
experiments: {
outputModule: true
},
};

View file

@ -2,13 +2,12 @@
set -e set -e
rm -rf pkg wasm-pack build --target web --features wasm,wee_alloc
cd reconcile-js
npm run build
mkdir -p ../examples/website/dist
cp -R dist/index.js ../examples/website/dist/index.js
wasm-pack build --target web --features wasm cd ../examples/website
cp -R pkg/reconcile.js examples/website/
cp -R pkg/reconcile_bg.wasm examples/website/
cd examples/website/
python3 -m http.server $1 --bind 0.0.0.0 python3 -m http.server $1 --bind 0.0.0.0

8
scripts/lint.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
set -e
cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged
cargo fmt --all
echo "Success!"

16
scripts/test.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
set -e
wasm-pack build --target web --features wasm,wee_alloc
cargo test --verbose
cargo test --features serde
cargo test --features wasm,wee_alloc
wasm-pack test --node --features wasm,wee_alloc
cd reconcile-js
npm install
npm run test
cd -
echo "Success!"

View file

@ -1,2 +0,0 @@
pub mod myers;
pub mod raw_operation;

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