Compare commits

...

12 Commits
0.1.1 ... main

26 changed files with 1140 additions and 338 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
*~
.DS_Store
*.swp
/target
**/target
**/Cargo.lock
woweb/assets/dist_text*
dist/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

18
.idea/misc.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MakefileSettings">
<option name="linkedExternalProjectsSettings">
<MakefileProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="version" value="2" />
</MakefileProjectSettings>
</option>
</component>
<component name="MakefileWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,10 +1,22 @@
pipeline:
check:
image: docker.io/rust:1.67-alpine3.17
commands:
- apk --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing add cargo-deny make
- make check
release:
image: docker.io/rust:1.67-alpine3.17
when:
event: tag
commands:
- apk --no-cache add wasm-pack make
- make release
publish-docker-image:
image: plugins/kaniko
when:
event: tag
settings:
repo: docker.io/thallian/woweb-poc
repo: docker.io/thallian/woweb
tags: latest,${CI_COMMIT_SHA:0:8},${CI_COMMIT_TAG=pre}
dockerfile: Dockerfile
username:

439
Cargo.lock generated
View File

@ -3,10 +3,19 @@
version = 3
[[package]]
name = "async-trait"
version = "0.1.64"
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "async-trait"
version = "0.1.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc"
dependencies = [
"proc-macro2",
"quote",
@ -21,9 +30,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.7"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fb79c228270dcf2426e74864cabc94babb5dbab01a4314e702d2f16540e1591"
checksum = "8582122b8edba2af43eaf6b80dbfd33f421b5a0eb3a3113d21bc096ac5b44faf"
dependencies = [
"async-trait",
"axum-core",
@ -51,16 +60,16 @@ dependencies = [
"tokio",
"tokio-tungstenite",
"tower",
"tower-http",
"tower-http 0.4.0",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34"
checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e"
dependencies = [
"async-trait",
"bytes",
@ -87,7 +96,7 @@ dependencies = [
"pin-project-lite",
"tokio",
"tower",
"tower-http",
"tower-http 0.3.5",
"tower-layer",
"tower-service",
]
@ -112,13 +121,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.3"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -146,6 +161,24 @@ dependencies = [
"libc",
]
[[package]]
name = "crdts"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e30e1170960eddf5392c448ff5b190a49cfcb28d6c79f789f2b4e30b571f8f8"
dependencies = [
"num",
"quickcheck",
"serde",
"tiny-keccak",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -166,6 +199,33 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dist_text"
version = "0.1.0"
dependencies = [
"crdts",
]
[[package]]
name = "dist_text_js"
version = "0.1.0"
dependencies = [
"dist_text",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
]
[[package]]
name = "env_logger"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"log",
"regex",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -280,6 +340,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.8"
@ -288,7 +359,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -404,6 +475,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "js-sys"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -480,8 +560,8 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.45.0",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
@ -494,6 +574,85 @@ dependencies = [
"winapi",
]
[[package]]
name = "num"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
"serde",
]
[[package]]
name = "num-complex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
dependencies = [
"num-traits",
"serde",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-bigint",
"num-integer",
"num-traits",
"serde",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
@ -536,7 +695,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.45.0",
"windows-sys",
]
[[package]]
@ -592,6 +751,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quickcheck"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f"
dependencies = [
"env_logger",
"log",
"rand 0.7.3",
"rand_core 0.5.1",
]
[[package]]
name = "quote"
version = "1.0.23"
@ -601,6 +772,19 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -608,8 +792,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
@ -619,7 +813,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
@ -628,7 +831,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.8",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@ -646,6 +858,8 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
@ -666,15 +880,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "rustversion"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]]
name = "ryu"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "scopeguard"
@ -684,18 +898,29 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.152"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
name = "serde-wasm-bindgen"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217"
dependencies = [
"proc-macro2",
"quote",
@ -704,9 +929,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
dependencies = [
"itoa",
"ryu",
@ -715,9 +940,9 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341"
checksum = "db0969fff533976baadd92e08b1d102c5a3d8a8049eadfd69d4d1e3c5b2ed189"
dependencies = [
"serde",
]
@ -780,9 +1005,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
@ -790,9 +1015,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.107"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
@ -807,18 +1032,18 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "thiserror"
version = "1.0.38"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
dependencies = [
"proc-macro2",
"quote",
@ -835,6 +1060,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -852,9 +1086,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [
"autocfg",
"bytes",
@ -867,7 +1101,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@ -942,12 +1176,30 @@ dependencies = [
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
@ -1041,7 +1293,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.8.5",
"sha1",
"thiserror",
"url",
@ -1065,15 +1317,15 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c"
[[package]]
name = "unicode-ident"
version = "1.0.6"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "unicode-normalization"
@ -1084,12 +1336,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "url"
version = "2.3.1"
@ -1129,12 +1375,72 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "winapi"
version = "0.3.9"
@ -1157,21 +1463,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@ -1239,18 +1530,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "woweb-poc"
version = "0.1.0"
name = "woweb"
version = "0.2.0"
dependencies = [
"axum",
"axum-extra",
"dist_text",
"futures",
"headers",
"serde",
"serde_json",
"tokio",
"tower-http",
"tower-http 0.3.5",
"tracing",
"tracing-subscriber",
"unicode-segmentation",
]

View File

@ -1,17 +1,10 @@
[package]
name = "woweb-poc"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
axum = { version = "0.6.7", features = ["ws", "headers"] }
axum-extra = { version = "0.5.0", features = ["spa"] }
futures = "0.3.26"
headers = "0.3.8"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
tokio = { version = "1.25.0", features = ["full"] }
tower-http = { version = "0.3.5", features = ["trace"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
unicode-segmentation = "1.10.1"
members = [
"dist_text",
"dist_text_js",
"woweb"
]
[profile.release]
strip = true

View File

@ -1,26 +1,29 @@
FROM docker.io/rust:1.67-alpine3.17 AS builder
RUN apk add --no-cache \
musl-dev
musl-dev \
wasm-pack \
make
RUN rustup target add wasm32-unknown-unknown
ADD . /src
WORKDIR /src
RUN cargo build --release
RUN strip target/release/woweb-poc
RUN make release
RUN adduser -D woweb
RUN sed -i 's|ws://localhost:3000/ws|wss://woweb.vanwa.ch/ws|' assets/index.js
RUN sed -i 's|ws://localhost:3000/ws|wss://woweb.vanwa.ch/ws|' dist/assets/index.js
FROM scratch
FROM docker.io/alpine:3.17
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /src/target/release/woweb-poc "/opt/woweb-poc"
COPY --from=builder /src/assets "/opt/assets"
COPY --from=builder /src/dist/woweb "/opt/woweb"
COPY --from=builder /src/dist/assets "/opt/assets"
EXPOSE 3000
WORKDIR /opt
USER woweb
CMD ["/opt/woweb-poc", "0.0.0.0:3000"]
CMD ["/opt/woweb", "0.0.0.0:3000"]

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
WASM_PKG_DIR = dist_text_js/pkg
ASSET_DIR = woweb/assets
WASM_JS = dist_text_js.js
WASM_BG = dist_text_js_bg.wasm
WASM_JS_PKG = $(WASM_PKG_DIR)/$(WASM_JS)
WASM_BG_PKG = $(WASM_PKG_DIR)/$(WASM_BG)
WASM_JS_ASSET = $(ASSET_DIR)/$(WASM_JS)
WASM_BG_ASSET = $(ASSET_DIR)/$(WASM_BG)
DIST_DIR = dist
.PHONY: default all check wasm serve clean
default: all
all: wasm
wasm: $(WASM_JS_ASSET) $(WASM_BG_ASSET)
$(WASM_JS_ASSET) $(WASM_BG_ASSET): dist_text_js/src/lib.rs
cd dist_text_js; \
wasm-pack build --target web --release --no-typescript
cp $(WASM_JS_PKG) $(WASM_JS_ASSET)
cp $(WASM_BG_PKG) $(WASM_BG_ASSET)
check:
cargo deny check
serve: wasm
cd woweb; \
cargo run
release: $(DIST_DIR) wasm
cargo build --package woweb --release
cp target/release/woweb $(DIST_DIR)
cp -r $(ASSET_DIR) $(DIST_DIR)
$(DIST_DIR):
mkdir -p $(DIST_DIR)
clean:
cargo clean
rm -rf $(WASM_PKG_DIR)
rm -f $(ASSET_DIR)/dist_text*

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Prerequisites
- [make](https://en.wikipedia.org/wiki/Make_(software))
- [wasm-pack](https://github.com/rustwasm/wasm-pack)
- [rustup](https://rustup.rs/)
- [cargo](https://github.com/rust-lang/cargo)
```
rustup target add wasm32-unknown-unknown
```

View File

@ -1,86 +0,0 @@
const areaId = "doctext";
const validEvents = [
"insertText",
"insertFromPaste",
"insertLineBreak",
"deleteContentBackward",
"deleteContentForward",
];
let selectionStart = 0;
let selectionEnd = 0;
let area;
let ws;
const uuid = self.crypto.randomUUID();
const wsUrl = "ws://localhost:3000/ws";
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
function setup() {
area = document.querySelector(`#${areaId}`);
ws = new WebSocket(wsUrl);
setupUi();
setupWs();
}
function setupUi() {
document.addEventListener("selectionchange", onSelectionChange, false);
area.addEventListener("beforeinput", onInput, false);
}
function setupWs() {
ws.onclose = function (e) {
console.log(e);
setTimeout(() => {
ws = new WebSocket(wsUrl);
}, 2000);
};
ws.onmessage = function (e) {
let payload = JSON.parse(e.data);
if (payload.client !== uuid) {
console.log(payload.doc);
area.value = payload.doc;
}
};
}
function onSelectionChange() {
const activeElement = document.activeElement;
if (activeElement && activeElement.id === areaId) {
console.debug("onSelectionChange", area);
selectionStart = area.selectionStart;
selectionEnd = area.selectionEnd;
}
}
function onInput(event) {
if (!validEvents.includes(event.inputType)) return;
const payload = {
client: uuid,
action: event.inputType,
data: event.data,
start: selectionStart,
end: selectionEnd,
};
ws.send(JSON.stringify(payload));
console.log(selectionStart, selectionEnd);
// workaround for differences between firefox and chrome
// chrome does not fire selectionchange events on backspace/delete events,
// while firefox does
if (event.inputType === "deleteContentBackward") {
selectionStart = area.selectionStart - 1;
selectionEnd = area.selectionEnd - 1;
}
}
setup();

264
deny.toml Normal file
View File

@ -0,0 +1,264 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
#"MIT",
#"Apache-2.0",
#"Apache-2.0 WITH LLVM-exception",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "allow"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "both"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
{ allow = ["CC0-1.0"], name = "tiny-keccak", version = "2.0.2" },
{ allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "1.0.8" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overriden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overriden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#name = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]

8
dist_text/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "dist_text"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
crdts = "7.3.0"

3
dist_text/src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod text;
pub use crdts;

197
dist_text/src/text.rs Normal file
View File

@ -0,0 +1,197 @@
use crdts::list::Op;
use crdts::{CmRDT, List};
use std::fmt::{Display, Formatter};
#[derive(Clone)]
pub struct Text {
inner: List<u16, String>,
}
impl Default for Text {
fn default() -> Self {
Self { inner: List::new() }
}
}
impl Display for Text {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str_bytes: Vec<u16> = self.inner.to_owned().read_into();
let str_repr = String::from_utf16_lossy(&str_bytes);
write!(f, "{}", str_repr)
}
}
impl Text {
pub fn apply_ops(&mut self, ops: Vec<Op<u16, String>>) {
ops.iter()
.for_each(move |op| self.inner.apply(op.to_owned()));
}
pub fn insert_linebreak(
&mut self,
start: usize,
end: usize,
src: &str,
) -> Vec<Op<u16, String>> {
self.insert(start, end, "\n", src)
}
pub fn insert(
&mut self,
start: usize,
end: usize,
data: &str,
src: &str,
) -> Vec<Op<u16, String>> {
let mut ops: Vec<Op<u16, String>> = Vec::new();
if start < end {
let mut delete_ops = self.delete(start, end, src);
ops.append(&mut delete_ops);
}
let data: Vec<u16> = data.encode_utf16().collect();
let mut insert_idx = start;
for byte in data.iter() {
let op = self.inner.insert_index(insert_idx, *byte, src.to_owned());
self.inner.apply(op.to_owned());
insert_idx += 1;
ops.push(op);
}
ops
}
pub fn delete_backward(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
if (start == 0 && start == end) || end > self.inner.len() {
return Vec::new();
}
let (end, start) = if start == end {
(start, start - 1)
} else {
(end, start)
};
self.delete(start, end, src)
}
pub fn delete_forward(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
if start >= self.inner.len() {
return Vec::new();
}
let end = if start == end { start + 1 } else { end };
self.delete(start, end, src)
}
pub fn delete(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
let mut start_idx = start;
let mut end_idx = end;
while let Some(byte) = self.inner.position(start_idx) {
if !matches!(byte, 0xDC00..=0xDFFF) {
break;
}
start_idx -= 1;
}
if let Some(end_byte) = self.inner.position(end_idx - 1) {
if end_idx < self.inner.len() && end_byte > &0xD800 && end_byte < &0xDC00 {
end_idx += 1;
}
}
let mut ops: Vec<Op<u16, String>> = Vec::new();
for _ in start_idx..end_idx {
if let Some(op) = self.inner.delete_index(start_idx, src.to_owned()) {
self.inner.apply(op.to_owned());
ops.push(op);
}
}
ops
}
}
#[cfg(test)]
mod tests {
use crate::text::Text;
#[test]
fn insert_delete() {
let mut t = Text::default();
t.insert(0, 0, "Hello", "A");
t.insert(5, 5, "world!", "A");
t.insert(5, 5, ", ", "B");
assert_eq!(t.to_string(), "Hello, world!");
t.insert(7, 12, "🌍", "A");
assert_eq!(t.to_string(), "Hello, 🌍!");
t.delete(8, 9, "B");
t.insert(7, 7, "🚀", "C");
assert_eq!(t.to_string(), "Hello, 🚀!");
}
#[test]
fn backspace() {
let mut t = Text::default();
t.insert(0, 0, "Hello", "A");
t.delete_backward(5, 5, "A");
assert_eq!(t.to_string(), "Hell");
t.delete_backward(0, 4, "A");
assert_eq!(t.to_string(), "");
}
#[test]
fn delete() {
let mut t = Text::default();
t.insert(0, 0, "Hello", "A");
t.delete_forward(4, 4, "A");
assert_eq!(t.to_string(), "Hell");
t.delete_forward(0, 4, "A");
assert_eq!(t.to_string(), "");
}
#[test]
fn linebreak() {
let mut t = Text::default();
t.insert(0, 0, "Hello, world!", "A");
t.insert_linebreak(6, 6, "A");
assert_eq!(t.to_string(), "Hello,\n world!");
t.delete_backward(7, 7, "B");
t.insert_linebreak(6, 7, "A");
assert_eq!(t.to_string(), "Hello,\nworld!");
}
#[test]
fn apply_ops() {
let mut t_a = Text::default();
let ops_a = t_a.insert(0, 0, "Hello, world!", "A");
let mut t_b = Text::default();
t_b.apply_ops(ops_a);
assert_eq!(t_b.to_string(), "Hello, world!");
let ops_a = t_a.insert(7, 7, "distributed ", "A");
t_b.apply_ops(ops_a);
let ops_b = t_b.insert(7, 7, "cruel ", "B");
t_a.apply_ops(ops_b);
assert_eq!(t_a.to_string(), "Hello, cruel distributed world!");
assert_eq!(t_b.to_string(), "Hello, cruel distributed world!");
}
}

14
dist_text_js/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "dist_text_js"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[lib]
crate-type = ["cdylib"]
[dependencies]
dist_text = { path = "../dist_text" }
serde = { version = "1.0.153", features = ["derive"] }
serde-wasm-bindgen = "0.5.0"
wasm-bindgen = "0.2.84"

38
dist_text_js/src/lib.rs Normal file
View File

@ -0,0 +1,38 @@
use wasm_bindgen::prelude::*;
use dist_text::crdts::list::Op;
use dist_text::text::Text;
#[wasm_bindgen(getter_with_clone)]
pub struct State {
pub text: String,
#[wasm_bindgen(skip)]
pub inner: Text,
}
#[wasm_bindgen]
impl State {
// wasm does not support trait impls yet, hence the default implementation here
pub fn default() -> Self {
Self { text: String::default(), inner: Text::default() }
}
pub fn apply(&mut self, ops: JsValue) -> String {
let ops: Vec<Op<u16, String>> = serde_wasm_bindgen::from_value(ops).unwrap_or(Vec::new());
self.inner.apply_ops(ops);
self.inner.to_string()
}
pub fn execute(&mut self, action: &str, start: usize, end: usize, data: &str, src: &str) -> JsValue {
let ops = match action {
"insertText" | "insertFromPaste" => self.inner.insert(start, end, data, src),
"insertLineBreak" => self.inner.insert_linebreak(start, end, src),
"deleteContentBackward" => self.inner.delete_backward(start, end, src),
"deleteContentForward" => self.inner.delete_forward(start, end, src),
&_ => Vec::default()
};
serde_wasm_bindgen::to_value(&ops).unwrap_or_default()
}
}

View File

@ -1,48 +0,0 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::actions::delete::{DeleteContentBackward, DeleteContentForward};
use crate::actions::insert::{InsertText, InsertFromPaste, InsertLineBreak};
use crate::actions::ActionRunner;
use crate::AppState;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum ActionType {
InsertText,
InsertFromPaste,
InsertLineBreak,
DeleteContentBackward,
DeleteContentForward,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct Action {
pub(crate) client: String,
data: Option<String>,
action: ActionType,
start: usize,
end: usize,
}
impl Action {
pub(crate) async fn execute(&self, state: Arc<AppState>) {
{
let mut doc = state.doc.write().await;
let action: Box<dyn ActionRunner> = match self.action {
ActionType::InsertText => Box::new(InsertText),
ActionType::InsertFromPaste => Box::new(InsertFromPaste),
ActionType::InsertLineBreak => Box::new(InsertLineBreak),
ActionType::DeleteContentBackward => Box::new(DeleteContentBackward),
ActionType::DeleteContentForward => Box::new(DeleteContentForward),
};
let data = match self.data.to_owned() {
None => Vec::new(),
Some(data) => data.encode_utf16().collect()
};
*doc = action.run(self.start, self.end, data, doc.to_owned());
}
}
}

View File

@ -1,45 +0,0 @@
use super::ActionRunner;
pub(crate) struct DeleteContentBackward;
pub(crate) struct DeleteContentForward;
fn delete(start: usize, end: usize, mut doc: Vec<u16>) -> Vec<u16> {
if start > doc.len() + 1 {
return doc;
}
let mut start_idx = start;
let mut end_idx = end;
unsafe {
while start_idx > 0 && matches!(doc.get_unchecked(start_idx), 0xDC00..=0xDFFF) {
start_idx -= 1;
}
let end_byte = doc.get_unchecked(end_idx - 1);
if end_idx < doc.len() && end_byte > &0xD800 && end_byte < &0xDC00 {
end_idx += 1;
}
}
doc.drain(start_idx..end_idx);
doc
}
impl ActionRunner for DeleteContentBackward {
fn run(&self, start: usize, end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
if (start == 0 && start == end ) || end > doc.len() { return doc; }
let (end, start) = if start == end { (start, start - 1) } else { (end, start) };
delete(start, end, doc)
}
}
impl ActionRunner for DeleteContentForward {
fn run(&self, start: usize, end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
if start >= doc.len() { return doc; }
let end = if start == end { start + 1 } else { end };
delete(start, end, doc)
}
}

View File

@ -1,38 +0,0 @@
use super::ActionRunner;
pub(crate) struct InsertText;
pub(crate) struct InsertFromPaste;
pub(crate) struct InsertLineBreak;
const NL_BYTES: [u16; 1] = [10];
fn insert(start: usize, end: usize, data: Vec<u16>, mut doc: Vec<u16>) -> Vec<u16> {
if doc.len() < start || data.is_empty() {
return doc;
}
if start < end {
doc.drain(start..end);
}
doc.splice(start..start, data);
doc
}
impl ActionRunner for InsertText {
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
insert(start, end, data, doc)
}
}
impl ActionRunner for InsertFromPaste {
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
insert(start, end, data, doc)
}
}
impl ActionRunner for InsertLineBreak {
fn run(&self, start: usize, _end: usize, _data: Vec<u16>, doc: Vec<u16>) -> Vec<u16> {
insert(start, start, Vec::from(NL_BYTES), doc)
}
}

View File

@ -1,6 +0,0 @@
pub(crate) mod delete;
pub(crate) mod insert;
pub(crate) trait ActionRunner {
fn run(&self, start: usize, end: usize, data: Vec<u16>, doc: Vec<u16>) -> Vec<u16>;
}

18
woweb/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "woweb"
version = "0.2.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
dist_text = { path = "../dist_text" }
axum = { version = "0.6.7", features = ["ws", "headers"] }
axum-extra = { version = "0.5.0", features = ["spa"] }
futures = "0.3.26"
headers = "0.3.8"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
tokio = { version = "1.25.0", features = ["full"] }
tower-http = { version = "0.3.5", features = ["trace"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }

View File

@ -6,6 +6,6 @@
</head>
<body>
<textarea id="doctext" name="story" rows="5" cols="33">Things just happen, what the hell.</textarea>
<script src="/index.js"></script>
<script type="module" src="/index.js"></script>
</body>
</html>

90
woweb/assets/index.js Normal file
View File

@ -0,0 +1,90 @@
import init, { State } from "/dist_text_js.js";
await init();
const areaId = "doctext";
const validEvents = [
"insertText",
"insertFromPaste",
"insertLineBreak",
"insertCompositionText",
"deleteContentBackward",
"deleteContentForward",
];
let selectionStart = 0;
let selectionEnd = 0;
let area;
let ws;
const uuid = self.crypto.randomUUID();
const wsUrl = "ws://localhost:3000/ws";
const state = State.default();
function setup() {
area = document.querySelector(`#${areaId}`);
ws = new WebSocket(wsUrl);
setupUi();
setupWs();
}
function setupUi() {
document.addEventListener("selectionchange", onSelectionChange, false);
area.addEventListener("beforeinput", onInput, false);
}
function setupWs() {
ws.onclose = function (e) {
console.log(e);
setTimeout(() => {
ws = new WebSocket(wsUrl);
}, 2000);
};
ws.onmessage = function (e) {
let payload = JSON.parse(e.data);
if (payload.client !== uuid) {
const newText = state.apply(payload.ops);
state.text = payload.ops.length > 0 ? newText : payload.doc;
area.value = state.text;
}
};
}
function onSelectionChange() {
const activeElement = document.activeElement;
if (activeElement && activeElement.id === areaId) {
selectionStart = area.selectionStart;
selectionEnd = area.selectionEnd;
}
}
function onInput(event) {
const data = event.data ? event.data : "";
let ops = state.execute(event.inputType, selectionStart, selectionEnd, data, uuid);
if (!validEvents.includes(event.inputType)) return;
const payload = {
client: uuid,
ops
};
ws.send(JSON.stringify(payload));
console.log(selectionStart, selectionEnd);
// workaround for differences between firefox and chrome
// chrome does not fire selectionchange events on backspace/delete events,
// while firefox does
if (event.inputType === "deleteContentBackward") {
selectionStart = area.selectionStart - 1;
selectionEnd = area.selectionEnd - 1;
}
}
setup();

View File

@ -1,6 +1,7 @@
use axum::routing::get;
use axum::Router;
use axum_extra::routing::SpaRouter;
use dist_text::text::Text;
use std::env;
use std::net::SocketAddr;
use std::sync::Arc;
@ -8,12 +9,10 @@ use tokio::sync::{broadcast, RwLock};
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod action;
mod actions;
mod ws;
struct AppState {
doc: RwLock<Vec<u16>>,
doc: RwLock<Text>,
tx: broadcast::Sender<String>,
}
@ -29,7 +28,7 @@ async fn main() {
let (tx, _rx) = broadcast::channel(100);
let app_state = Arc::new(AppState {
doc: RwLock::new(Vec::new()),
doc: RwLock::new(Text::default()),
tx,
});

View File

@ -9,14 +9,22 @@ use axum::{
response::IntoResponse,
};
use dist_text::crdts::list::Op;
use futures::{sink::SinkExt, stream::StreamExt};
use crate::{action::Action, AppState};
use crate::AppState;
#[derive(Serialize, Deserialize)]
struct NewState {
client: String,
doc: String,
ops: Vec<Op<u16, String>>,
}
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct Ops {
pub(crate) client: String,
ops: Vec<Op<u16, String>>,
}
pub(crate) async fn route(
@ -31,10 +39,11 @@ async fn handle_socket(stream: WebSocket, state: Arc<AppState>) {
{
let doc = state.doc.read().await;
let doc: String = String::from_utf16_lossy(&doc);
let doc: String = doc.to_string();
let new_state = NewState {
client: String::from("0000"),
doc: doc.to_string(),
ops: Vec::new(),
};
let payload = serde_json::to_string(&new_state).unwrap();
let _ = sender.send(Message::Text(payload)).await;
@ -54,17 +63,17 @@ async fn handle_socket(stream: WebSocket, state: Arc<AppState>) {
let state = state.clone();
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(Message::Text(text))) = receiver.next().await {
if let Ok(action) = serde_json::from_str::<Action>(&text) {
let client = action.client.clone();
tracing::debug!("update from {}", client.clone());
action.execute(state.clone()).await;
if let Ok(new_ops) = serde_json::from_str::<Ops>(&text) {
tracing::debug!("update from {}", new_ops.client.clone());
let doc = state.doc.read().await;
let mut doc = doc.to_owned();
doc.apply_ops(new_ops.ops.to_owned());
let new_state = NewState {
client,
doc: String::from_utf16_lossy(&doc),
client: new_ops.client,
doc: doc.to_string(),
ops: new_ops.ops,
};
let payload = serde_json::to_string(&new_state).unwrap();
let _ = tx.send(payload);