on master help sign in
75 files changed+6294−2453
since splitsnapshots
unifiedsplit
.gitignore+1 .travis.yml+19−9 Cargo.lock+647−322 Cargo.toml+51−14 README.md+60−23 benches/benches.rs+118 build.sh+2−2 crates/js-api/Cargo.toml+6−7 crates/js-api/src/lib.rs+30−38 examples/counter/Cargo.toml+5−5 examples/counter/README.md+1−1 examples/counter/index.html+3−3 examples/counter/src/lib.rs+10−13 examples/game-of-life/Cargo.toml+5−6 examples/game-of-life/README.md+1−1 examples/game-of-life/index.html+3−6 examples/game-of-life/src/lib.rs+25−23 examples/hello-world/Cargo.toml+3−3 examples/hello-world/README.md+1−1 examples/hello-world/index.html+3−3 examples/hello-world/src/lib.rs+12−14 examples/input-form/Cargo.toml+3−3 examples/input-form/README.md+1−1 examples/input-form/index.html+3−3 examples/input-form/src/lib.rs+10−11 examples/js-component/Cargo.toml+3−3 examples/js-component/README.md+1−1 examples/js-component/index.html+3−3 examples/js-component/src/lib.rs+6−10 examples/moire/Cargo.toml+8−9 examples/moire/README.md+1−1 examples/moire/index.html+3−3 examples/moire/src/lib.rs+38−64 examples/sierpinski-triangle/Cargo.toml+28 examples/sierpinski-triangle/README.md+24 examples/sierpinski-triangle/ferris.png examples/sierpinski-triangle/index.html+69 examples/sierpinski-triangle/src/lib.rs+157 examples/todomvc/Cargo.toml+11−12 examples/todomvc/README.md+1−1 examples/todomvc/index.html+3−3 examples/todomvc/src/router.rs+23−21 examples/todomvc/src/todo.rs+86−32 examples/todomvc/src/todos.rs+40−56 js/change-list-interpreter.js+377 js/change-list.js−268 js/package-lock.json−5 js/package.json−11 js/strace.js+80 publish.sh+11 src/builder.rs+161−6 src/cached.rs+139−73 src/cached_set.rs+186 src/change_list.rs−426 src/change_list/emitter.rs+292 src/change_list/js.rs+40 src/change_list/mod.rs+353 src/change_list/strings.rs+66 src/change_list/traversal.rs+344 src/diff.rs+1125 src/events.rs+123−83 src/lib.rs+64−11 src/node.rs+150−77 src/render.rs+65−50 src/render_context.rs+104 src/strace.rs+22 src/vdom.rs+182−338 tests/web/cached.js+11 tests/web/cached.rs+239−51 tests/web/events.rs+58−156 tests/web/js_api.rs+37−47 tests/web/keyed.js+25 tests/web/keyed.rs+269 tests/web/main.rs+60−37 tests/web/render.rs+183−83
.gitignore+1
@@ -1,3 +1,4 @@
11/target
22**/*.rs.bk
33wasm-pack.log
4+/.vscode
.travis.yml+19−9
@@ -25,20 +25,30 @@ matrix:
2525 - name: "build (beta)"
2626 rust: beta
2727 script: INSTALL=1 ./build.sh
28− - name: "build (nightly)"
29− rust: nightly
30− script: INSTALL=1 ./build.sh
3128 - name: "headless browser tests (firefox)"
3229 rust: stable
3330 addons:
3431 firefox: latest
35− script: wasm-pack test --firefox --headless
36− - name: "headless browser tests (chrome)"
37− rust: stable
38− addons:
39− chrome: stable
40− script: wasm-pack test --chrome --headless
32+ script: wasm-pack test --firefox --headless -- --features xxx-unstable-internal-use-only
4133 - name: "native tests"
4234 rust: stable
4335 install: echo "no install"
4436 script: cargo test --all
37+
38+ # TODO: A bug in `rustc` is making `js-sys` broken on nightly:
39+ # https://github.com/rust-lang/rust/issues/69315
40+ # - name: "build (nightly)"
41+ # rust: nightly
42+ # script: INSTALL=1 ./build.sh
43+ # - name: "check native benches"
44+ # rust: nightly
45+ # install: echo "no install"
46+ # script: cargo check --benches --features xxx-unstable-internal-use-only
47+
48+ # TODO: `chromedriver` is complaining about ports or something. Temporarily
49+ # disable in CI until we figure it out.
50+ # - name: "headless browser tests (chrome)"
51+ # rust: stable
52+ # addons:
53+ # chrome: stable
54+ # script: wasm-pack test --chrome --headless -- --features xxx-unstable-internal-use-only
Cargo.lock+647−322
@@ -1,104 +1,204 @@
11# This file is automatically @generated by Cargo.
22# It is not intended for manual editing.
33[[package]]
4−name = "aho-corasick"
5−version = "0.6.10"
4+name = "anyhow"
5+version = "1.0.25"
66source = "registry+https://github.com/rust-lang/crates.io-index"
7−dependencies = [
8− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
9−]
107
118[[package]]
129name = "atty"
13−version = "0.2.11"
10+version = "0.2.13"
1411source = "registry+https://github.com/rust-lang/crates.io-index"
1512dependencies = [
16− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
17− "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
18− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
13+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
14+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
1915]
2016
2117[[package]]
2218name = "autocfg"
23−version = "0.1.2"
19+version = "0.1.7"
2420source = "registry+https://github.com/rust-lang/crates.io-index"
2521
2622[[package]]
27−name = "backtrace"
28−version = "0.3.14"
23+name = "bitflags"
24+version = "1.2.1"
2925source = "registry+https://github.com/rust-lang/crates.io-index"
30−dependencies = [
31− "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
32− "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
33− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
34− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
35− "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
36− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
37−]
3826
3927[[package]]
40−name = "backtrace-sys"
41−version = "0.1.28"
28+name = "bstr"
29+version = "0.2.8"
4230source = "registry+https://github.com/rust-lang/crates.io-index"
4331dependencies = [
44− "cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)",
45− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
32+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
33+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
34+ "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
35+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
4636]
4737
4838[[package]]
4939name = "bumpalo"
50−version = "2.1.0"
40+version = "2.6.0"
5141source = "registry+https://github.com/rust-lang/crates.io-index"
5242
5343[[package]]
5444name = "byteorder"
55−version = "1.3.1"
45+version = "1.3.2"
5646source = "registry+https://github.com/rust-lang/crates.io-index"
5747
5848[[package]]
59−name = "cc"
60−version = "1.0.30"
49+name = "cast"
50+version = "0.2.3"
6151source = "registry+https://github.com/rust-lang/crates.io-index"
52+dependencies = [
53+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
54+]
6255
6356[[package]]
6457name = "cfg-if"
65−version = "0.1.7"
58+version = "0.1.10"
6659source = "registry+https://github.com/rust-lang/crates.io-index"
6760
61+[[package]]
62+name = "clap"
63+version = "2.33.0"
64+source = "registry+https://github.com/rust-lang/crates.io-index"
65+dependencies = [
66+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
67+ "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
68+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
69+]
70+
6871[[package]]
6972name = "console_error_panic_hook"
7073version = "0.1.6"
7174source = "registry+https://github.com/rust-lang/crates.io-index"
7275dependencies = [
73− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
74− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
76+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
77+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
7578]
7679
7780[[package]]
7881name = "console_log"
79−version = "0.1.2"
82+version = "0.2.0"
83+source = "registry+https://github.com/rust-lang/crates.io-index"
84+dependencies = [
85+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
86+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
87+]
88+
89+[[package]]
90+name = "criterion"
91+version = "0.3.3"
92+source = "registry+https://github.com/rust-lang/crates.io-index"
93+dependencies = [
94+ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
95+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
96+ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
97+ "criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
98+ "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
99+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
100+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
101+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
102+ "oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
103+ "plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
104+ "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
105+ "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
106+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
107+ "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
108+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
109+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
110+ "tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
111+ "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
112+]
113+
114+[[package]]
115+name = "criterion-plot"
116+version = "0.4.3"
117+source = "registry+https://github.com/rust-lang/crates.io-index"
118+dependencies = [
119+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
120+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
121+]
122+
123+[[package]]
124+name = "crossbeam-deque"
125+version = "0.7.2"
126+source = "registry+https://github.com/rust-lang/crates.io-index"
127+dependencies = [
128+ "crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
129+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
130+]
131+
132+[[package]]
133+name = "crossbeam-epoch"
134+version = "0.8.0"
135+source = "registry+https://github.com/rust-lang/crates.io-index"
136+dependencies = [
137+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
138+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
139+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
141+ "memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
142+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
143+]
144+
145+[[package]]
146+name = "crossbeam-queue"
147+version = "0.2.0"
148+source = "registry+https://github.com/rust-lang/crates.io-index"
149+dependencies = [
150+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
151+]
152+
153+[[package]]
154+name = "crossbeam-utils"
155+version = "0.7.0"
156+source = "registry+https://github.com/rust-lang/crates.io-index"
157+dependencies = [
158+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
159+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
160+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
161+]
162+
163+[[package]]
164+name = "csv"
165+version = "1.1.1"
166+source = "registry+https://github.com/rust-lang/crates.io-index"
167+dependencies = [
168+ "bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
169+ "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
170+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
171+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
172+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
173+]
174+
175+[[package]]
176+name = "csv-core"
177+version = "0.1.6"
80178source = "registry+https://github.com/rust-lang/crates.io-index"
81179dependencies = [
82− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
83− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
180+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
84181]
85182
86183[[package]]
87184name = "dodrio"
88−version = "0.1.0"
185+version = "0.2.0"
89186dependencies = [
90− "bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
91− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
92− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
93− "dodrio-js-api 0.1.0",
94− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
187+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
188+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
189+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
190+ "criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
191+ "dodrio-js-api 0.2.0",
192+ "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
95193 "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
96− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
97− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
98− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
99− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
100− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
101− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
194+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
195+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
196+ "longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
197+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
198+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
199+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
200+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
201+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
102202]
103203
104204[[package]]
@@ -106,12 +206,12 @@ name = "dodrio-counter"
106206version = "0.1.0"
107207dependencies = [
108208 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
109− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
110− "dodrio 0.1.0",
111− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
112− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
113− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
114− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
209+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
210+ "dodrio 0.2.0",
211+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
212+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
213+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
214+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
115215]
116216
117217[[package]]
@@ -119,13 +219,12 @@ name = "dodrio-game-of-life"
119219version = "0.1.0"
120220dependencies = [
121221 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
122− "dodrio 0.1.0",
123− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
124− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
125− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
126− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
127− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
128− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
222+ "dodrio 0.2.0",
223+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
224+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
225+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
226+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
227+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
129228]
130229
131230[[package]]
@@ -133,10 +232,10 @@ name = "dodrio-hello-world"
133232version = "0.1.0"
134233dependencies = [
135234 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
136− "dodrio 0.1.0",
137− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
138− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
139− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
235+ "dodrio 0.2.0",
236+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
237+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
238+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
140239]
141240
142241[[package]]
@@ -144,22 +243,21 @@ name = "dodrio-input-form"
144243version = "0.1.0"
145244dependencies = [
146245 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
147− "dodrio 0.1.0",
148− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
149− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
150− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
246+ "dodrio 0.2.0",
247+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
248+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
249+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
151250]
152251
153252[[package]]
154253name = "dodrio-js-api"
155−version = "0.1.0"
254+version = "0.2.0"
156255dependencies = [
157− "dodrio 0.1.0",
158− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
159− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
160− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
161− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
162− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
256+ "dodrio 0.2.0",
257+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
258+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
259+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
260+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
163261]
164262
165263[[package]]
@@ -167,322 +265,515 @@ name = "dodrio-js-component"
167265version = "0.1.0"
168266dependencies = [
169267 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
170− "dodrio 0.1.0",
171− "dodrio-js-api 0.1.0",
172− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
173− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
174− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
268+ "dodrio 0.2.0",
269+ "dodrio-js-api 0.2.0",
270+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
271+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
272+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
175273]
176274
177275[[package]]
178276name = "dodrio-moire"
179277version = "0.1.0"
180278dependencies = [
181− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
279+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
182280 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
183− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
184− "dodrio 0.1.0",
185− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
186− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
187− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
188− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
189− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
190− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
191− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
281+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
282+ "dodrio 0.2.0",
283+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
284+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
285+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
286+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
287+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
288+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
289+]
290+
291+[[package]]
292+name = "dodrio-sierpinski"
293+version = "0.1.0"
294+dependencies = [
295+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
296+ "dodrio 0.2.0",
297+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
298+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
299+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
300+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
301+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
192302]
193303
194304[[package]]
195305name = "dodrio-todomvc"
196306version = "0.1.0"
197307dependencies = [
198− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
308+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
199309 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
200− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
201− "dodrio 0.1.0",
202− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
203− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
204− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
205− "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
206− "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
207− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
208− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
209− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
210− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
310+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
311+ "dodrio 0.2.0",
312+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
313+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
314+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
315+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
316+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
317+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
318+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
319+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
211320]
212321
213322[[package]]
214−name = "env_logger"
215−version = "0.6.1"
323+name = "either"
324+version = "1.5.3"
325+source = "registry+https://github.com/rust-lang/crates.io-index"
326+
327+[[package]]
328+name = "futures"
329+version = "0.3.5"
216330source = "registry+https://github.com/rust-lang/crates.io-index"
217331dependencies = [
218− "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
219− "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
220− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
221− "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
222− "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
332+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
333+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
334+ "futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
335+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
336+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
337+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
338+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
223339]
224340
225341[[package]]
226−name = "failure"
227−version = "0.1.5"
342+name = "futures-channel"
343+version = "0.3.5"
228344source = "registry+https://github.com/rust-lang/crates.io-index"
229345dependencies = [
230− "backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
231− "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
346+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
347+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
232348]
233349
234350[[package]]
235−name = "failure_derive"
236−version = "0.1.5"
351+name = "futures-core"
352+version = "0.3.5"
353+source = "registry+https://github.com/rust-lang/crates.io-index"
354+
355+[[package]]
356+name = "futures-executor"
357+version = "0.3.5"
237358source = "registry+https://github.com/rust-lang/crates.io-index"
238359dependencies = [
239− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
240− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
241− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
242− "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
360+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
361+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
362+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
243363]
244364
245365[[package]]
246−name = "futures"
247−version = "0.1.25"
366+name = "futures-io"
367+version = "0.3.5"
368+source = "registry+https://github.com/rust-lang/crates.io-index"
369+
370+[[package]]
371+name = "futures-macro"
372+version = "0.3.5"
373+source = "registry+https://github.com/rust-lang/crates.io-index"
374+dependencies = [
375+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
376+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
377+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
378+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
379+]
380+
381+[[package]]
382+name = "futures-sink"
383+version = "0.3.5"
248384source = "registry+https://github.com/rust-lang/crates.io-index"
249385
386+[[package]]
387+name = "futures-task"
388+version = "0.3.5"
389+source = "registry+https://github.com/rust-lang/crates.io-index"
390+dependencies = [
391+ "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
392+]
393+
394+[[package]]
395+name = "futures-util"
396+version = "0.3.5"
397+source = "registry+https://github.com/rust-lang/crates.io-index"
398+dependencies = [
399+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
400+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
401+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
402+ "futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
403+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
404+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
405+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
406+ "pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
407+ "pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
408+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
409+ "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
410+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
411+]
412+
250413[[package]]
251414name = "fxhash"
252415version = "0.2.1"
253416source = "registry+https://github.com/rust-lang/crates.io-index"
254417dependencies = [
255− "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
418+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
256419]
257420
421+[[package]]
422+name = "half"
423+version = "1.6.0"
424+source = "registry+https://github.com/rust-lang/crates.io-index"
425+
258426[[package]]
259427name = "heck"
260428version = "0.3.1"
261429source = "registry+https://github.com/rust-lang/crates.io-index"
262430dependencies = [
263− "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
431+ "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
432+]
433+
434+[[package]]
435+name = "hermit-abi"
436+version = "0.1.3"
437+source = "registry+https://github.com/rust-lang/crates.io-index"
438+dependencies = [
439+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
264440]
265441
266442[[package]]
267−name = "humantime"
268−version = "1.2.0"
443+name = "itertools"
444+version = "0.9.0"
269445source = "registry+https://github.com/rust-lang/crates.io-index"
270446dependencies = [
271− "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
447+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
272448]
273449
274450[[package]]
275451name = "itoa"
276−version = "0.4.3"
452+version = "0.4.4"
277453source = "registry+https://github.com/rust-lang/crates.io-index"
278454
279455[[package]]
280456name = "js-sys"
281−version = "0.3.15"
457+version = "0.3.32"
282458source = "registry+https://github.com/rust-lang/crates.io-index"
283459dependencies = [
284− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
460+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
285461]
286462
287463[[package]]
288464name = "lazy_static"
289−version = "1.3.0"
465+version = "1.4.0"
290466source = "registry+https://github.com/rust-lang/crates.io-index"
291467
292468[[package]]
293469name = "libc"
294−version = "0.2.50"
470+version = "0.2.66"
295471source = "registry+https://github.com/rust-lang/crates.io-index"
296472
297473[[package]]
298474name = "log"
299−version = "0.4.6"
475+version = "0.4.11"
300476source = "registry+https://github.com/rust-lang/crates.io-index"
301477dependencies = [
302− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
478+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
303479]
304480
481+[[package]]
482+name = "longest-increasing-subsequence"
483+version = "0.1.0"
484+source = "registry+https://github.com/rust-lang/crates.io-index"
485+
305486[[package]]
306487name = "memchr"
307−version = "2.2.0"
488+version = "2.2.1"
308489source = "registry+https://github.com/rust-lang/crates.io-index"
490+dependencies = [
491+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
492+]
493+
494+[[package]]
495+name = "memoffset"
496+version = "0.5.3"
497+source = "registry+https://github.com/rust-lang/crates.io-index"
498+dependencies = [
499+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
500+]
309501
310502[[package]]
311503name = "nom"
312−version = "4.2.2"
504+version = "4.2.3"
313505source = "registry+https://github.com/rust-lang/crates.io-index"
314506dependencies = [
315− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
507+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
316508 "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
317509]
318510
319511[[package]]
320−name = "proc-macro2"
321−version = "0.4.27"
512+name = "num-traits"
513+version = "0.2.10"
322514source = "registry+https://github.com/rust-lang/crates.io-index"
323515dependencies = [
324− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
516+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
325517]
326518
327519[[package]]
328−name = "quick-error"
329−version = "1.2.2"
520+name = "num_cpus"
521+version = "1.11.1"
330522source = "registry+https://github.com/rust-lang/crates.io-index"
523+dependencies = [
524+ "hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
525+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
526+]
527+
528+[[package]]
529+name = "once_cell"
530+version = "1.3.1"
531+source = "registry+https://github.com/rust-lang/crates.io-index"
532+
533+[[package]]
534+name = "oorandom"
535+version = "11.1.0"
536+source = "registry+https://github.com/rust-lang/crates.io-index"
537+
538+[[package]]
539+name = "pin-project"
540+version = "0.4.16"
541+source = "registry+https://github.com/rust-lang/crates.io-index"
542+dependencies = [
543+ "pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
544+]
545+
546+[[package]]
547+name = "pin-project-internal"
548+version = "0.4.16"
549+source = "registry+https://github.com/rust-lang/crates.io-index"
550+dependencies = [
551+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
552+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
553+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
554+]
555+
556+[[package]]
557+name = "pin-utils"
558+version = "0.1.0"
559+source = "registry+https://github.com/rust-lang/crates.io-index"
560+
561+[[package]]
562+name = "plotters"
563+version = "0.2.12"
564+source = "registry+https://github.com/rust-lang/crates.io-index"
565+dependencies = [
566+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
567+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
568+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
569+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
570+]
571+
572+[[package]]
573+name = "proc-macro-hack"
574+version = "0.5.11"
575+source = "registry+https://github.com/rust-lang/crates.io-index"
576+dependencies = [
577+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
578+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
579+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
580+]
581+
582+[[package]]
583+name = "proc-macro-nested"
584+version = "0.1.3"
585+source = "registry+https://github.com/rust-lang/crates.io-index"
586+
587+[[package]]
588+name = "proc-macro2"
589+version = "1.0.18"
590+source = "registry+https://github.com/rust-lang/crates.io-index"
591+dependencies = [
592+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
593+]
331594
332595[[package]]
333596name = "quote"
334−version = "0.6.11"
597+version = "1.0.2"
335598source = "registry+https://github.com/rust-lang/crates.io-index"
336599dependencies = [
337− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
600+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
338601]
339602
340603[[package]]
341−name = "redox_syscall"
342−version = "0.1.51"
604+name = "rayon"
605+version = "1.3.0"
343606source = "registry+https://github.com/rust-lang/crates.io-index"
607+dependencies = [
608+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
609+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
610+ "rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
611+]
344612
345613[[package]]
346−name = "redox_termios"
347−version = "0.1.1"
614+name = "rayon-core"
615+version = "1.7.0"
348616source = "registry+https://github.com/rust-lang/crates.io-index"
349617dependencies = [
350− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
618+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
619+ "crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
620+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
621+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
622+ "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
351623]
352624
353625[[package]]
354626name = "regex"
355−version = "1.1.2"
627+version = "1.3.3"
356628source = "registry+https://github.com/rust-lang/crates.io-index"
357629dependencies = [
358− "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
359− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
360− "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
361− "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
362− "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
630+ "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
363631]
364632
365633[[package]]
366−name = "regex-syntax"
367−version = "0.6.5"
634+name = "regex-automata"
635+version = "0.1.8"
368636source = "registry+https://github.com/rust-lang/crates.io-index"
369637dependencies = [
370− "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
638+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
371639]
372640
373641[[package]]
374−name = "rustc-demangle"
375−version = "0.1.13"
642+name = "regex-syntax"
643+version = "0.6.13"
376644source = "registry+https://github.com/rust-lang/crates.io-index"
377645
646+[[package]]
647+name = "rustc_version"
648+version = "0.2.3"
649+source = "registry+https://github.com/rust-lang/crates.io-index"
650+dependencies = [
651+ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
652+]
653+
378654[[package]]
379655name = "ryu"
380−version = "0.2.7"
656+version = "1.0.2"
381657source = "registry+https://github.com/rust-lang/crates.io-index"
382658
659+[[package]]
660+name = "same-file"
661+version = "1.0.5"
662+source = "registry+https://github.com/rust-lang/crates.io-index"
663+dependencies = [
664+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
665+]
666+
383667[[package]]
384668name = "scoped-tls"
385669version = "1.0.0"
386670source = "registry+https://github.com/rust-lang/crates.io-index"
387671
388672[[package]]
389−name = "serde"
390−version = "1.0.89"
673+name = "scopeguard"
674+version = "1.0.0"
391675source = "registry+https://github.com/rust-lang/crates.io-index"
392−dependencies = [
393− "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
394−]
395676
396677[[package]]
397−name = "serde_derive"
398−version = "1.0.89"
678+name = "semver"
679+version = "0.9.0"
399680source = "registry+https://github.com/rust-lang/crates.io-index"
400681dependencies = [
401− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
402− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
403− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
682+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
404683]
405684
406685[[package]]
407−name = "serde_json"
408−version = "1.0.39"
686+name = "semver-parser"
687+version = "0.7.0"
409688source = "registry+https://github.com/rust-lang/crates.io-index"
410−dependencies = [
411− "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
412− "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
413− "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
414−]
415689
416690[[package]]
417−name = "sourcefile"
418−version = "0.1.4"
691+name = "serde"
692+version = "1.0.114"
419693source = "registry+https://github.com/rust-lang/crates.io-index"
694+dependencies = [
695+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
696+]
420697
421698[[package]]
422−name = "syn"
423−version = "0.15.29"
699+name = "serde_cbor"
700+version = "0.11.1"
424701source = "registry+https://github.com/rust-lang/crates.io-index"
425702dependencies = [
426− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
427− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
428− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
703+ "half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
704+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
429705]
430706
431707[[package]]
432−name = "synstructure"
433−version = "0.10.1"
708+name = "serde_derive"
709+version = "1.0.114"
434710source = "registry+https://github.com/rust-lang/crates.io-index"
435711dependencies = [
436− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
437− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
438− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
439− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
712+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
713+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
714+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
440715]
441716
442717[[package]]
443−name = "termcolor"
444−version = "1.0.4"
718+name = "serde_json"
719+version = "1.0.57"
445720source = "registry+https://github.com/rust-lang/crates.io-index"
446721dependencies = [
447− "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
722+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
723+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
724+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
448725]
449726
450727[[package]]
451−name = "termion"
452−version = "1.5.1"
728+name = "slab"
729+version = "0.4.2"
730+source = "registry+https://github.com/rust-lang/crates.io-index"
731+
732+[[package]]
733+name = "sourcefile"
734+version = "0.1.4"
735+source = "registry+https://github.com/rust-lang/crates.io-index"
736+
737+[[package]]
738+name = "syn"
739+version = "1.0.33"
453740source = "registry+https://github.com/rust-lang/crates.io-index"
454741dependencies = [
455− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
456− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
457− "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
742+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
743+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
744+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
458745]
459746
460747[[package]]
461−name = "thread_local"
462−version = "0.3.6"
748+name = "textwrap"
749+version = "0.11.0"
463750source = "registry+https://github.com/rust-lang/crates.io-index"
464751dependencies = [
465− "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
752+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
466753]
467754
468755[[package]]
469−name = "ucd-util"
470−version = "0.1.3"
756+name = "tinytemplate"
757+version = "1.1.0"
471758source = "registry+https://github.com/rust-lang/crates.io-index"
759+dependencies = [
760+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
761+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
762+]
472763
473764[[package]]
474765name = "unicode-segmentation"
475−version = "1.2.1"
766+version = "1.6.0"
476767source = "registry+https://github.com/rust-lang/crates.io-index"
477768
478769[[package]]
479−name = "unicode-xid"
480−version = "0.1.0"
770+name = "unicode-width"
771+version = "0.1.6"
481772source = "registry+https://github.com/rust-lang/crates.io-index"
482773
483774[[package]]
484−name = "utf8-ranges"
485−version = "1.0.2"
775+name = "unicode-xid"
776+version = "0.2.0"
486777source = "registry+https://github.com/rust-lang/crates.io-index"
487778
488779[[package]]
@@ -490,125 +781,136 @@ name = "version_check"
490781version = "0.1.5"
491782source = "registry+https://github.com/rust-lang/crates.io-index"
492783
784+[[package]]
785+name = "walkdir"
786+version = "2.3.1"
787+source = "registry+https://github.com/rust-lang/crates.io-index"
788+dependencies = [
789+ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
790+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
791+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
792+]
793+
493794[[package]]
494795name = "wasm-bindgen"
495−version = "0.2.38"
796+version = "0.2.55"
496797source = "registry+https://github.com/rust-lang/crates.io-index"
497798dependencies = [
498− "wasm-bindgen-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
799+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
800+ "wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
499801]
500802
501803[[package]]
502804name = "wasm-bindgen-backend"
503−version = "0.2.38"
805+version = "0.2.55"
504806source = "registry+https://github.com/rust-lang/crates.io-index"
505807dependencies = [
506− "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
507− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
508− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
509− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
510− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
511− "wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
808+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
809+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
810+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
811+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
812+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
813+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
814+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
512815]
513816
514817[[package]]
515818name = "wasm-bindgen-futures"
516−version = "0.3.15"
819+version = "0.4.5"
517820source = "registry+https://github.com/rust-lang/crates.io-index"
518821dependencies = [
519− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
520− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
521− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
822+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
823+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
824+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
825+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
522826]
523827
524828[[package]]
525829name = "wasm-bindgen-macro"
526−version = "0.2.38"
830+version = "0.2.55"
527831source = "registry+https://github.com/rust-lang/crates.io-index"
528832dependencies = [
529− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
530− "wasm-bindgen-macro-support 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
833+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
834+ "wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
531835]
532836
533837[[package]]
534838name = "wasm-bindgen-macro-support"
535−version = "0.2.38"
839+version = "0.2.55"
536840source = "registry+https://github.com/rust-lang/crates.io-index"
537841dependencies = [
538− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
539− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
540− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
541− "wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
542− "wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
842+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
843+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
844+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
845+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
846+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
543847]
544848
545849[[package]]
546850name = "wasm-bindgen-shared"
547−version = "0.2.38"
851+version = "0.2.55"
548852source = "registry+https://github.com/rust-lang/crates.io-index"
549853
550854[[package]]
551855name = "wasm-bindgen-test"
552−version = "0.2.38"
856+version = "0.3.5"
553857source = "registry+https://github.com/rust-lang/crates.io-index"
554858dependencies = [
555859 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
556− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
557− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
860+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
558861 "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
559− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
560− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
561− "wasm-bindgen-test-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
862+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
863+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
864+ "wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
562865]
563866
564867[[package]]
565868name = "wasm-bindgen-test-macro"
566−version = "0.2.38"
869+version = "0.3.5"
567870source = "registry+https://github.com/rust-lang/crates.io-index"
568871dependencies = [
569− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
570− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
872+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
873+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
571874]
572875
573876[[package]]
574877name = "wasm-bindgen-webidl"
575−version = "0.2.32"
878+version = "0.2.55"
576879source = "registry+https://github.com/rust-lang/crates.io-index"
577880dependencies = [
578− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
881+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
579882 "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
580− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
581− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
582− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
583− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
584− "wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
585− "weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
883+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
884+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
885+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
886+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
887+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
888+ "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
586889]
587890
588891[[package]]
589892name = "web-sys"
590−version = "0.3.15"
893+version = "0.3.32"
591894source = "registry+https://github.com/rust-lang/crates.io-index"
592895dependencies = [
593− "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
594− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
595− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
896+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
897+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
596898 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
597− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
598− "wasm-bindgen-webidl 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
899+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
900+ "wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
599901]
600902
601903[[package]]
602904name = "weedle"
603−version = "0.8.0"
905+version = "0.10.0"
604906source = "registry+https://github.com/rust-lang/crates.io-index"
605907dependencies = [
606− "nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
908+ "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
607909]
608910
609911[[package]]
610912name = "winapi"
611−version = "0.3.6"
913+version = "0.3.8"
612914source = "registry+https://github.com/rust-lang/crates.io-index"
613915dependencies = [
614916 "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -625,7 +927,7 @@ name = "winapi-util"
625927version = "0.1.2"
626928source = "registry+https://github.com/rust-lang/crates.io-index"
627929dependencies = [
628− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
930+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
629931]
630932
631933[[package]]
@@ -633,78 +935,101 @@ name = "winapi-x86_64-pc-windows-gnu"
633935version = "0.4.0"
634936source = "registry+https://github.com/rust-lang/crates.io-index"
635937
636−[[package]]
637−name = "wincolor"
638−version = "1.0.1"
639−source = "registry+https://github.com/rust-lang/crates.io-index"
640−dependencies = [
641− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
642− "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
643−]
644−
645938[metadata]
646−"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
647−"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
648−"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
649−"checksum backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a90e2b463010cd0e0ce9a11d4a9d5d58d9f41d4a6ba3dcaf9e68b466e88b4"
650−"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
651−"checksum bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "21ef2109240a377370f55ea3ef0b486b46d7b5c0f7455ab0ec676d73f875d58a"
652−"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
653−"checksum cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "d01c69d08ff207f231f07196e30f84c70f1c815b04f980f8b7b01ff01f05eb92"
654−"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
939+"checksum anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14"
940+"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
941+"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
942+"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
943+"checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245"
944+"checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708"
945+"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
946+"checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
947+"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
948+"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
655949"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
656−"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48"
657−"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a"
658−"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
659−"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
660−"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
950+"checksum console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494"
951+"checksum criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
952+"checksum criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
953+"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
954+"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
955+"checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700"
956+"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
957+"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d"
958+"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c"
959+"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
960+"checksum futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
961+"checksum futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
962+"checksum futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
963+"checksum futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
964+"checksum futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
965+"checksum futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
966+"checksum futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
967+"checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
968+"checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
661969"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
970+"checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
662971"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
663−"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
664−"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
665−"checksum js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "09636f30233c2623a9b4bb05506e224733550e2a13b32ab560dc1744fd5103ca"
666−"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
667−"checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1"
668−"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
669−"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
670−"checksum nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22293d25d3f33a8567cc8a1dc20f40c7eeb761ce83d0fcca059858580790cac3"
671−"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
672−"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
673−"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
674−"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
675−"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
676−"checksum regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53ee8cfdddb2e0291adfb9f13d31d3bbe0a03c9a402c01b1e24188d86c35b24f"
677−"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
678−"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
679−"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
972+"checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120"
973+"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
974+"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
975+"checksum js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "1c840fdb2167497b0bd0db43d6dfe61e91637fa72f9d061f8bd17ddc44ba6414"
976+"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
977+"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
978+"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
979+"checksum longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
980+"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
981+"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
982+"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
983+"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
984+"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
985+"checksum once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b"
986+"checksum oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
987+"checksum pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "81d480cb4e89522ccda96d0eed9af94180b7a5f93fb28f66e1fd7d68431663d1"
988+"checksum pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "a82996f11efccb19b685b14b5df818de31c1edcee3daa256ab5775dd98e72feb"
989+"checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
990+"checksum plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb"
991+"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
992+"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e"
993+"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
994+"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
995+"checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098"
996+"checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9"
997+"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
998+"checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9"
999+"checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
1000+"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
1001+"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
1002+"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421"
6801003"checksum scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
681−"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560"
682−"checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c"
683−"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
1004+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
1005+"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
1006+"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
1007+"checksum serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
1008+"checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
1009+"checksum serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
1010+"checksum serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)" = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
1011+"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
6841012"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
685−"checksum syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1825685f977249735d510a242a6727b46efe914bb67e38d30c071b1b72b1d5c2"
686−"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
687−"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
688−"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
689−"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
690−"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
691−"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
692−"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
693−"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
1013+"checksum syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
1014+"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
1015+"checksum tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
1016+"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
1017+"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
1018+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
6941019"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
695−"checksum wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "f7148f7446b911dd8e8d490a4fd44bf7fcd6caa21ba2fed9aef7bf34b9a974d4"
696−"checksum wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "07b7b7e0a5c244e02469d7ad0f071dca0a01a7be217db41e4de5e05d4a1ab2ac"
697−"checksum wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "ad790bdb3818ba6fba63d3a3fd65ab52ca7f601cad1c6c1feda66199b4e9f161"
698−"checksum wasm-bindgen-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "e51818f0ab6cdef85125d2c2f1a89096370286fe229d24de89504617918d76af"
699−"checksum wasm-bindgen-macro-support 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "4770fd638bed46ce4669cb59aba4657e264839c24c568cc03d772849bfaed7eb"
700−"checksum wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6f52047198d1887ffd4e508264bffbf92382a97c8b09b74781dbb11ee055ccc5"
701−"checksum wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "3f551eb8ad779620e0bb424f6936c87aaea8d93db72fc6d6292c179306773b6f"
702−"checksum wasm-bindgen-test-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "5d7bf67f27d40d702d1c85778abaa354142b77ef00528b72127bb392b8db39e2"
703−"checksum wasm-bindgen-webidl 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "264e2ff77974c11640a2c6a91435f645843d2bd7e12e3ce58ac74209e6d5f76d"
704−"checksum web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a9c8ff79e13b74f47f438cd48de2ed808c419cd52ec4780dfc6d1de4a37be19"
705−"checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
706−"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
1020+"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
1021+"checksum wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "29ae32af33bacd663a9a28241abecf01f2be64e6a185c6139b04f18b6385c5f2"
1022+"checksum wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "1845584bd3593442dc0de6e6d9f84454a59a057722f36f005e44665d6ab19d85"
1023+"checksum wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1458706aa1b8fe6898d19433c9f110d93a05d1f22ae6adf55810409a94df34b4"
1024+"checksum wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "87fcc747e6b73c93d22c947a6334644d22cfec5abd8b66238484dc2b0aeb9fe4"
1025+"checksum wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3dc4b3f2c4078c8c4a5f363b92fcf62604c5913cbd16c6ff5aaf0f74ec03f570"
1026+"checksum wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "ca0b78d6d3be8589b95d1d49cdc0794728ca734adf36d7c9f07e6459508bb53d"
1027+"checksum wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ed5833682054e941db0b7e2b4877b61bbaafd680608e40d268e070d34558b305"
1028+"checksum wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c5dcf70d21d0ff0ea5cdb97dba9a9b56ddad4a9bd10c8912eedc55618be022f"
1029+"checksum wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3126356474ceb717c8fb5549ae387c9fbf4872818454f4d87708bee997214bb5"
1030+"checksum web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "98405c0a2e722ed3db341b4c5b70eb9fe0021621f7350bab76df93b09b649bbf"
1031+"checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
1032+"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
7071033"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
7081034"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
7091035"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
710−"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
Cargo.toml+51−14
@@ -8,23 +8,52 @@ license = "MPL-2.0"
88name = "dodrio"
99readme = "./README.md"
1010repository = "https://github.com/fitzgen/dodrio"
11−version = "0.1.0"
11+version = "0.2.0"
1212
1313[badges]
1414travis-ci = { repository = "fitzgen/dodrio" }
1515
16+[lib]
17+path = "src/lib.rs"
18+bench = false
19+
20+[[test]]
21+name = "web"
22+path = "tests/web/main.rs"
23+required-features = ["xxx-unstable-internal-use-only"]
24+
25+[[bench]]
26+name = "benches"
27+path = "benches/benches.rs"
28+harness = false
29+required-features = ["xxx-unstable-internal-use-only"]
30+
31+[features]
32+# Don't rely on this feature! Its behavior will change and it could go away at
33+# any time. This enables monkey-patching DOM methods so that we wrap them and
34+# trace what DOM methods are used and how many times they are invoked when
35+# applying our emitted change lists to the physical DOM. This is useful for
36+# evaluating the differences between how Dodrio applies changes from virtual
37+# nodes to the physical DOM versus how other virtual DOM libraries do the same.
38+xxx-unstable-strace = []
39+
40+# Don't use this feature! It only exists for exposing otherwise-internal APIs
41+# for testing and benchmarks.
42+xxx-unstable-internal-use-only = []
43+
1644[dependencies]
17−bumpalo = "2.1.0"
18−cfg-if = "0.1.7"
19−futures = "0.1.25"
20−wasm-bindgen = "0.2.38"
21−wasm-bindgen-futures = "0.3.15"
22−js-sys = "0.3.15"
45+bumpalo = "2.6.0"
46+cfg-if = "0.1.10"
2347fxhash = "0.2.1"
24−log = { version = "0.4.6", optional = true }
48+js-sys = "0.3.32"
49+log = { version = "0.4.11", optional = true }
50+longest-increasing-subsequence = "0.1.0"
51+serde = { version = "1.0.114", optional = true }
52+wasm-bindgen = "0.2.55"
53+wasm-bindgen-futures = "0.4.5"
2554
2655[dependencies.web-sys]
27−version = "0.3.15"
56+version = "0.3.32"
2857features = [
2958 "console",
3059 "Document",
@@ -35,13 +64,14 @@ features = [
3564]
3665
3766[dev-dependencies]
38−console_log = "0.1.2"
39−dodrio-js-api = { version = "=0.1.0", path = "./crates/js-api" }
40−log = "0.4.6"
41−wasm-bindgen-test = "0.2.38"
67+console_log = "0.2.0"
68+dodrio-js-api = { version = "=0.2.0", path = "./crates/js-api" }
69+futures = "0.3.5"
70+log = "0.4.11"
71+wasm-bindgen-test = "0.3.5"
4272
4373[dev-dependencies.web-sys]
44−version = "0.3.15"
74+version = "0.3.32"
4575features = [
4676 "Attr",
4777 "EventTarget",
@@ -50,6 +80,12 @@ features = [
5080 "NodeList",
5181]
5282
83+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
84+criterion = "0.3.3"
85+
86+[profile.bench]
87+debug = true
88+
5389[profile.release]
5490incremental = false
5591lto = true
@@ -64,5 +100,6 @@ members = [
64100 "./examples/input-form",
65101 "./examples/js-component",
66102 "./examples/moire",
103+ "./examples/sierpinski-triangle",
67104 "./examples/todomvc",
68105]
README.md+60−23
@@ -1,15 +1,14 @@
1−# `dodrio`
1+# Dodrio
22
3−`dodrio` is an **experimental** virtual DOM library for Rust and WebAssembly. It
4−is a proving ground for a bump allocation-based virtual DOM architecture, that I
5−believe is the best way to take advantage of WebAssembly's strengths in the
6−context of a virtual DOM library.
3+A fast, bump-allocated virtual DOM library for Rust and WebAssembly. Note that
4+Dodrio is still **experimental**.
75
86<!-- START doctoc generated TOC please keep comment here to allow auto update -->
97<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
108
119
1210- [Warning](#warning)
11+- [Examples](#examples)
1312- [Design](#design)
1413 - [Bump Allocation](#bump-allocation)
1514 - [Change List as Stack Machine](#change-list-as-stack-machine)
@@ -19,11 +18,53 @@ context of a virtual DOM library.
1918
2019## Warning
2120
22−I reiterate that `dodrio` is in a very **experimental** state. It has not
23−actually been profiled or tuned for performance yet, so while I think the design
24−should yield a very fast virtual DOM library, `dodrio` is almost certainly not
25−fast right now. Additionally, it is probably riddled with bugs, and is assuredly
26−missing features that are critical for actually building Web applications.
21+I reiterate that Dodrio is in a very **experimental** state. It probably has
22+bugs, and no one is using it in production.
23+
24+## Examples
25+
26+Here is the classic "Hello, World!" example:
27+
28+```rust
29+struct Hello {
30+ who: String,
31+}
32+
33+impl Render for Hello {
34+ fn render<'a>(&self, cx: &mut RenderContext<a>) -> Node<'a> {
35+ let who = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
36+ div(cx)
37+ .children([text(who.into_bump_str())])
38+ .finish()
39+ }
40+}
41+```
42+
43+More examples can be found in [the `examples`
44+directory](https://github.com/fitzgen/dodrio/tree/master/examples), including:
45+
46+* [`counter`](https://github.com/fitzgen/dodrio/tree/master/examples/counter):
47+ Incrementing and decrementing a counter.
48+* [`input-form`](https://github.com/fitzgen/dodrio/tree/master/examples/input-form):
49+ Reading an `<input>` and displaying its contents.
50+* [`todomvc`](https://github.com/fitzgen/dodrio/tree/master/examples/todomvc):
51+ An implementation of the infamous TodoMVC application.
52+* [`moire`](https://github.com/fitzgen/dodrio/tree/master/examples/moire): The
53+ WebRender Moiré patterns demo.
54+* [`game-of-life`](https://github.com/fitzgen/dodrio/tree/master/examples/game-of-life):
55+ The Rust and WebAssembly book's Game of Life tutorial rendered with Dodrio
56+ instead of to 2D canvas.
57+* [`js-component`](https://github.com/fitzgen/dodrio/tree/master/examples/js-component):
58+ Defines a rendering component in JavaScript with the `dodrio-js-api` crate.
59+
60+## Cargo Features
61+
62+* `log` &mdash; enable debugging-oriented log messages with the `log` crate's
63+ facade. You still have to initialize a logger for the messages to go anywhere,
64+ such as [`console_log`](https://github.com/iamcodemaker/console_log).
65+
66+* `serde` &mdash; enable `serde::{Serialize, Deserialize}` implementations for
67+ `Cached<R>` where `R` is serializable and deserializable.
2768
2869## Design
2970
@@ -33,7 +74,7 @@ Bump allocation is essentially the fastest method of allocating objects. It has
3374constraints, but works particularly well when allocation lifetimes match program
3475phases. And virtual DOMs are very phase oriented.
3576
36−`dodrio` maintains three bump allocation arenas:
77+Dodrio maintains three bump allocation arenas:
3778
38791. The newest, most up-to-date virtual DOM. The virtual DOM nodes themselves and
3980 any temporary containers needed while creating them are allocated into this
@@ -46,12 +87,12 @@ phases. And virtual DOMs are very phase oriented.
4687
4788Rendering happens as follows:
4889
49−* The application state is rendered into bump allocation arena (1).
50−* (1) diffed with (2) and the changes are emitted into (3).
51−* JavaScript code applies the change list in (3) to the physical DOM.
52−* (1) and (2) are swapped, double-buffering style, and the new (1) has its bump
53− allocation pointer reset, as does (3).
54−* Rinse and repeat.
90+1. The application state is rendered into bump allocation arena (1).
91+2. (1) is diffed with (2) and the changes are emitted into (3).
92+3. JavaScript code applies the change list in (3) to the physical DOM.
93+4. (1) and (2) are swapped, double-buffering style, and the new (1) has its bump
94+ allocation pointer reset, as does (3).
95+5. Rinse and repeat.
5596
5697### Change List as Stack Machine
5798
@@ -62,10 +103,6 @@ diffs, a task that is essentially a tree traversal.
62103
63104### Library — Not Framework
64105
65−`dodrio` is just a library. (And did I mention it is experimental?!) It is not a
106+Dodrio is just a library. (And did I mention it is experimental?!) It is not a
66107full-fledged, complete, batteries-included solution for all frontend Web
67−development. And it never intends to become that either. Its highest ambition is
68−to prove that its bump allocation-based design is a good one, and maaaayyyyyybe
69−become a production-grade virtual DOM library that you could plug into a larger
70−application or toolkit eventually. But it will never be a complete,
71−batteries-included framework.
108+development. And it never intends to become that either.
benches/benches.rsnew file+118
@@ -0,0 +1,118 @@
1+#![cfg(all(
2+ feature = "xxx-unstable-internal-use-only",
3+ not(target_arch = "wasm32")
4+))]
5+
6+use criterion::{
7+ black_box, criterion_group, criterion_main, Benchmark, Criterion, ParameterizedBenchmark,
8+ Throughput,
9+};
10+use dodrio::{
11+ builder::*,
12+ bumpalo::{self, Bump},
13+ CachedSet, Node, Render, RenderContext, Vdom,
14+};
15+use std::cell::RefCell;
16+use std::convert::TryInto;
17+
18+/// The simplest thing we can render: `<div/>`.
19+struct Empty;
20+impl<'a> Render<'a> for Empty {
21+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
22+ div(&cx).finish()
23+ }
24+}
25+
26+/// Render a list that is `self.0` items long, has attributes and listeners.
27+struct SimpleList(usize);
28+impl<'a> Render<'a> for SimpleList {
29+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
30+ let mut children = bumpalo::collections::Vec::with_capacity_in(self.0, cx.bump);
31+ children.extend((0..self.0).map(|_| {
32+ li(&cx)
33+ .attr("class", "my-list-item")
34+ .on("click", |_root, _vdom, _event| {
35+ panic!("no one should call this")
36+ })
37+ .children([text("a list item")])
38+ .finish()
39+ }));
40+ ol(&cx).attr("id", "my-list").children(children).finish()
41+ }
42+}
43+
44+fn criterion_benchmark(c: &mut Criterion) {
45+ c.bench(
46+ "render",
47+ Benchmark::new("empty", {
48+ let mut bump = Bump::new();
49+ let cached_set = RefCell::new(CachedSet::default());
50+ let mut templates = Default::default();
51+ move |b| {
52+ bump.reset();
53+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
54+ b.iter(|| {
55+ black_box(Empty.render(&mut cx));
56+ })
57+ }
58+ }),
59+ );
60+
61+ c.bench(
62+ "render",
63+ ParameterizedBenchmark::new(
64+ "list",
65+ {
66+ let mut bump = Bump::new();
67+ let cached_set = RefCell::new(CachedSet::default());
68+ let mut templates = Default::default();
69+ move |b, n| {
70+ bump.reset();
71+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
72+ b.iter(|| {
73+ black_box(SimpleList(*n).render(&mut cx));
74+ })
75+ }
76+ },
77+ vec![100, 1_000, 10_000],
78+ )
79+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
80+ );
81+
82+ c.bench(
83+ "render-and-diff",
84+ ParameterizedBenchmark::new(
85+ "same-list",
86+ |b, &n| {
87+ let vdom = Vdom::new(&(), SimpleList(n));
88+ b.iter(|| {
89+ vdom.immediately_render_and_diff(SimpleList(n));
90+ black_box(&vdom);
91+ })
92+ },
93+ vec![100, 1_000, 10_000],
94+ )
95+ .with_function("empty-to-full-list-to-empty", |b, &n| {
96+ b.iter(|| {
97+ let vdom = Vdom::new(&(), Empty);
98+ vdom.immediately_render_and_diff(SimpleList(n));
99+ black_box(&vdom);
100+ vdom.immediately_render_and_diff(Empty);
101+ black_box(&vdom);
102+ });
103+ })
104+ .with_function("append-one-and-remove-one", |b, &n| {
105+ let vdom = Vdom::new(&(), SimpleList(n));
106+ b.iter(|| {
107+ vdom.immediately_render_and_diff(SimpleList(n + 1));
108+ black_box(&vdom);
109+ vdom.immediately_render_and_diff(SimpleList(n));
110+ black_box(&vdom);
111+ })
112+ })
113+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
114+ );
115+}
116+
117+criterion_group!(benches, criterion_benchmark);
118+criterion_main!(benches);
build.sh+2−2
@@ -4,8 +4,8 @@ cd $(dirname $0)
44set -eux
55
66cargo fmt --all || true
7−cargo check --all --target wasm32-unknown-unknown
7+cargo build --all --target wasm32-unknown-unknown
88
99for x in ./examples/*; do
10− wasm-pack build --target no-modules "$x"
10+ wasm-pack build --target web "$x"
1111done
crates/js-api/Cargo.toml+6−7
@@ -7,17 +7,16 @@ keywords = ["javascript", "js", "vdom", "virtual-dom"]
77license = "MPL-2.0"
88name = "dodrio-js-api"
99repository = "https://github.com/fitzgen/dodrio"
10−version = "0.1.0"
10+version = "0.2.0"
1111
1212[badges]
1313travis-ci = { repository = "fitzgen/dodrio" }
1414
1515[dependencies]
16−dodrio = { version = "=0.1.0", path = "../.." }
17−wasm-bindgen = "0.2.38"
18−js-sys = "0.3.15"
19−wasm-bindgen-futures = "0.3.15"
20−futures = "0.1.25"
16+dodrio = { version = "=0.2.0", path = "../.." }
17+wasm-bindgen = "0.2.55"
18+js-sys = "0.3.32"
19+wasm-bindgen-futures = "0.4.5"
2120
2221[dev-dependencies]
23−wasm-bindgen-test = "0.2.38"
22+wasm-bindgen-test = "0.3.5"
crates/js-api/src/lib.rs+30−38
@@ -66,8 +66,7 @@ And here is a Rust rendering component that internally uses the JS rendering
6666component:
6767
6868```rust,no_run
69−use dodrio::bumpalo::Bump;
70−use dodrio::{Node, Render, Vdom};
69+use dodrio::{Node, Render, RenderContext, Vdom};
7170use dodrio_js_api::JsRender;
7271use js_sys::Object;
7372use wasm_bindgen::prelude::*;
@@ -100,16 +99,13 @@ impl GreetingViaJs {
10099
101100/// And finally the `Render` implementation! This adds a `<p>` element and some
102101/// text around whatever the inner JS `Greeting` component renders.
103−impl Render for GreetingViaJs {
104− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
105− where
106− 'a: 'bump,
107− {
102+impl<'a> Render<'a> for GreetingViaJs {
103+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
108104 use dodrio::builder::*;
109− p(bump)
105+ p(&cx)
110106 .children([
111107 text("JavaScript says: "),
112− self.js.render(bump),
108+ self.js.render(cx),
113109 ])
114110 .finish()
115111 }
@@ -119,12 +115,7 @@ impl Render for GreetingViaJs {
119115 */
120116#![deny(missing_docs, missing_debug_implementations)]
121117
122−use dodrio::{
123− builder,
124− bumpalo::{self, Bump},
125− Node, Render,
126−};
127−use futures::prelude::*;
118+use dodrio::{builder, bumpalo, Node, Render, RenderContext};
128119use js_sys::{Object, Promise, Reflect};
129120use wasm_bindgen::prelude::*;
130121use wasm_bindgen::JsCast;
@@ -193,12 +184,16 @@ impl VdomWeak {
193184 /// Schedule re-rendering of the virtual DOM. A promise is returned that is
194185 /// resolved after the rendering has happened.
195186 pub fn render(&self) -> Promise {
196− wasm_bindgen_futures::future_to_promise(
197− self.inner.render().map(|_| JsValue::null()).map_err(|e| {
187+ let future = self.inner.render();
188+
189+ wasm_bindgen_futures::future_to_promise(async move {
190+ if let Err(e) = future.await {
198191 let msg = e.to_string();
199− js_sys::Error::new(&msg).into()
200− }),
201− )
192+ Err(js_sys::Error::new(&msg).into())
193+ } else {
194+ Ok(JsValue::null())
195+ }
196+ })
202197 }
203198}
204199
@@ -221,12 +216,9 @@ impl JsRender {
221216 }
222217}
223218
224−impl Render for JsRender {
225− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
226− where
227− 'a: 'bump,
228− {
229− create(bump, self.render())
219+impl<'a> Render<'a> for JsRender {
220+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
221+ create(cx, self.render())
230222 }
231223}
232224
@@ -234,9 +226,9 @@ fn has_property(obj: &Object, property: &str) -> bool {
234226 Reflect::has(obj, &property.into()).unwrap_or_default()
235227}
236228
237−fn create(bump: &Bump, val: JsValue) -> Node {
229+fn create<'a>(cx: &mut RenderContext<'a>, val: JsValue) -> Node<'a> {
238230 if let Some(txt) = val.as_string() {
239− let text = bumpalo::collections::String::from_str_in(&txt, bump);
231+ let text = bumpalo::collections::String::from_str_in(&txt, cx.bump);
240232 return builder::text(text.into_bump_str());
241233 }
242234
@@ -251,12 +243,12 @@ fn create(bump: &Bump, val: JsValue) -> Node {
251243 );
252244
253245 let tag_name = elem.tag_name();
254− let tag_name = bumpalo::collections::String::from_str_in(&tag_name, bump);
246+ let tag_name = bumpalo::collections::String::from_str_in(&tag_name, cx.bump);
255247
256− builder::ElementBuilder::new(bump, tag_name.into_bump_str())
248+ builder::ElementBuilder::new(cx.bump, tag_name.into_bump_str())
257249 .listeners({
258250 let mut listeners =
259− bumpalo::collections::Vec::new_in(bump);
251+ bumpalo::collections::Vec::new_in(cx.bump);
260252 if has_property(&elem, "listeners") {
261253 let js_listeners = elem.listeners();
262254 listeners.reserve(js_listeners.length() as usize);
@@ -275,10 +267,10 @@ fn create(bump: &Bump, val: JsValue) -> Node {
275267 "listener objects returned by JS render methods must have an `callback` property"
276268 );
277269 let on = listener.on();
278− let on = bumpalo::collections::String::from_str_in(&on, bump);
270+ let on = bumpalo::collections::String::from_str_in(&on, cx.bump);
279271 let callback = listener.callback();
280272 let elem = elem.clone();
281− listeners.push(builder::on(bump, on.into_bump_str(), move |_root, vdom, event| {
273+ listeners.push(builder::on(cx.bump, on.into_bump_str(), move |_root, vdom, event| {
282274 let vdom = VdomWeak::new(vdom);
283275 let vdom: JsValue = vdom.into();
284276 if let Err(e) = callback.call2(&elem, &vdom, &event) {
@@ -290,7 +282,7 @@ fn create(bump: &Bump, val: JsValue) -> Node {
290282 listeners
291283 })
292284 .attributes({
293− let mut attributes = bumpalo::collections::Vec::new_in(bump);
285+ let mut attributes = bumpalo::collections::Vec::new_in(cx.bump);
294286 if has_property(&elem, "attributes") {
295287 let js_attributes = elem.attributes();
296288 attributes.reserve(js_attributes.length() as usize);
@@ -309,21 +301,21 @@ fn create(bump: &Bump, val: JsValue) -> Node {
309301 "attribute objects returned by JS render methods must have a `value` property"
310302 );
311303 let name = attribute.name();
312− let name = bumpalo::collections::String::from_str_in(&name, bump);
304+ let name = bumpalo::collections::String::from_str_in(&name, cx.bump);
313305 let value = attribute.value();
314− let value = bumpalo::collections::String::from_str_in(&value, bump);
306+ let value = bumpalo::collections::String::from_str_in(&value, cx.bump);
315307 attributes.push(builder::attr(name.into_bump_str(), value.into_bump_str()));
316308 });
317309 }
318310 attributes
319311 })
320312 .children({
321− let mut children = bumpalo::collections::Vec::new_in(bump);
313+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
322314 if has_property(&elem, "children") {
323315 let js_children = elem.children();
324316 children.reserve(js_children.length() as usize);
325317 js_children.for_each(&mut |child, _index, _array| {
326− children.push(create(bump, child));
318+ children.push(create(cx, child));
327319 });
328320 }
329321 children
examples/counter/Cargo.toml+5−5
@@ -11,13 +11,13 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313console_error_panic_hook = "0.1.6"
14−console_log = "0.1.2"
14+console_log = "0.2.0"
1515dodrio = { path = "../.." }
16−log = "0.4.6"
17−wasm-bindgen = "0.2.38"
16+log = "0.4.11"
17+wasm-bindgen = "0.2.55"
1818
1919[dependencies.web-sys]
20−version = "0.3.15"
20+version = "0.3.32"
2121features = [
2222 "console",
2323 "Document",
@@ -30,4 +30,4 @@ features = [
3030]
3131
3232[dev-dependencies]
33−wasm-bindgen-test = "0.2.38"
33+wasm-bindgen-test = "0.3.5"
examples/counter/README.md+1−1
@@ -9,7 +9,7 @@ See `src/lib.rs`.
99## Build
1010
1111```
12−wasm-pack build --target no-modules
12+wasm-pack build --target web
1313```
1414
1515## Serve
examples/counter/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Counter</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_counter.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_counter_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_counter.js";
10+ init("./pkg/dodrio_counter_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/counter/src/lib.rsmode 100644 → 100755+10−13
@@ -1,9 +1,8 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::Render;
1+use dodrio::{bumpalo, Node, Render, RenderContext};
32use log::*;
43use wasm_bindgen::prelude::*;
54
6−/// A counter that can be incremented and decrmented!
5+/// A counter that can be incremented and decremented!
76struct Counter {
87 count: isize,
98}
@@ -27,19 +26,16 @@ impl Counter {
2726
2827// The `Render` implementation for `Counter`s displays the current count and has
2928// buttons to increment and decrement the count.
30−impl Render for Counter {
31− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
32− where
33− 'a: 'bump,
34− {
29+impl<'a> Render<'a> for Counter {
30+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3531 use dodrio::builder::*;
3632
3733 // Stringify the count as a bump-allocated string.
38− let count = bumpalo::format!(in bump, "{}", self.count);
34+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
3935
40− div(bump)
36+ div(&cx)
4137 .children([
42− button(bump)
38+ button(&cx)
4339 .on("click", |root, vdom, _event| {
4440 // Cast the root render component to a `Counter`, since
4541 // we know that's what it is.
@@ -55,10 +51,11 @@ impl Render for Counter {
5551 .children([text("+")])
5652 .finish(),
5753 text(count.into_bump_str()),
58− button(bump)
54+ button(&cx)
5955 .on("click", |root, vdom, _event| {
6056 // Same as above, but decrementing instead of incrementing.
61− root.unwrap_mut::<Counter>().decrement();
57+ let counter = root.unwrap_mut::<Counter>();
58+ counter.decrement();
6259 vdom.schedule_render();
6360 })
6461 .children([text("-")])
examples/game-of-life/Cargo.toml+5−6
@@ -11,14 +11,13 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
16−js-sys = "0.3.15"
17−wasm-bindgen-futures = "0.3.15"
18−futures = "0.1.25"
16+js-sys = "0.3.32"
17+wasm-bindgen-futures = "0.4.5"
1918
2019[dependencies.web-sys]
21−version = "0.3.15"
20+version = "0.3.32"
2221features = [
2322 "Document",
2423 "HtmlElement",
@@ -27,4 +26,4 @@ features = [
2726]
2827
2928[dev-dependencies]
30−wasm-bindgen-test = "0.2.38"
29+wasm-bindgen-test = "0.3.5"
examples/game-of-life/README.md+1−1
@@ -11,7 +11,7 @@ See `src/lib.rs`.
1111## Build
1212
1313```
14−wasm-pack build --target no-modules
14+wasm-pack build --target web
1515```
1616
1717## Serve
examples/game-of-life/index.htmlmode 100644 → 100755+3−6
@@ -19,9 +19,6 @@
1919 padding: 0;
2020 }
2121
22− .universe {
23− }
24−
2522 body {
2623 width: 100vw;
2724 height: 100vh;
@@ -39,9 +36,9 @@
3936 </style>
4037 </head>
4138 <body>
42− <script src="pkg/dodrio_game_of_life.js"></script>
43− <script>
44− wasm_bindgen("pkg/dodrio_game_of_life_bg.wasm");
39+ <script type="module">
40+ import init from "./pkg/dodrio_game_of_life.js";
41+ init("./pkg/dodrio_game_of_life_bg.wasm");
4542 </script>
4643 </body>
4744</html>
examples/game-of-life/src/lib.rs+25−23
@@ -1,6 +1,4 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Node, Render, Vdom};
3−use futures::prelude::*;
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
42use std::cell::RefCell;
53use std::rc::Rc;
64use wasm_bindgen::{prelude::*, JsCast};
@@ -128,21 +126,19 @@ impl Universe {
128126}
129127
130128/// The rendering implementation for our Game of Life.
131−impl Render for Universe {
132− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
133− where
134− 'a: 'bump,
135− {
129+impl<'a> Render<'a> for Universe {
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
136131 use dodrio::builder::*;
137132
138− let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, bump);
133+ let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, cx.bump);
139134
140135 for row in self.cells.chunks(self.width as usize) {
141− let mut cells = bumpalo::collections::Vec::with_capacity_in(self.width as usize, bump);
136+ let mut cells =
137+ bumpalo::collections::Vec::with_capacity_in(self.width as usize, cx.bump);
142138
143139 for cell in row {
144140 cells.push(
145− span(bump)
141+ span(&cx)
146142 .attr("class", "cell")
147143 .attr(
148144 "style",
@@ -155,10 +151,10 @@ impl Render for Universe {
155151 );
156152 }
157153
158− rows.push(div(bump).attr("class", "row").children(cells).finish());
154+ rows.push(div(&cx).attr("class", "row").children(cells).finish());
159155 }
160156
161− div(bump).attr("class", "universe").children(rows).finish()
157+ div(&cx).attr("class", "universe").children(rows).finish()
162158 }
163159}
164160
@@ -175,26 +171,32 @@ pub fn run() {
175171 // Create a new Game of Life `Universe` render component.
176172 let universe = Universe::new();
177173
178− // Create a virtual DOM and mount it and the `Hello` render component to the
174+ // Create a virtual DOM and mount it and the `Universe` render component to the
179175 // `<body>`.
180176 let vdom = Vdom::new(body.as_ref(), universe);
181177
182178 // Kick off a loop that keeps computing a tick of the universe and then
183179 // re-rendering on every animation frame.
184− let rc: Rc<RefCell<Option<Closure<FnMut()>>>> = Rc::new(RefCell::new(None));
180+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
185181 let rc2 = rc.clone();
186182 let window2 = window.clone();
187183 let weak = vdom.weak();
188184 let f = Closure::wrap(Box::new(move || {
189185 weak.schedule_render();
190186
191− wasm_bindgen_futures::spawn_local(
192− weak.with_component(|root| {
193− let universe = root.unwrap_mut::<Universe>();
194− universe.tick();
195− })
196− .map_err(|_| wasm_bindgen::throw_str("impossible, we always keep the vdom alive")),
197− );
187+ wasm_bindgen_futures::spawn_local({
188+ let weak = weak.clone();
189+ async move {
190+ let fut = weak.with_component(|root| {
191+ let universe = root.unwrap_mut::<Universe>();
192+ universe.tick();
193+ });
194+
195+ if fut.await.is_err() {
196+ wasm_bindgen::throw_str("impossible, we always keep the vdom alive");
197+ }
198+ }
199+ });
198200
199201 window
200202 .request_animation_frame(
@@ -205,7 +207,7 @@ pub fn run() {
205207 .unchecked_ref::<js_sys::Function>(),
206208 )
207209 .unwrap_throw();
208− }) as Box<FnMut()>);
210+ }) as Box<dyn FnMut()>);
209211 window2
210212 .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
211213 .unwrap_throw();
examples/hello-world/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.15"
18+version = "0.3.32"
1919features = [
2020 "Document",
2121 "HtmlElement",
@@ -24,4 +24,4 @@ features = [
2424]
2525
2626[dev-dependencies]
27−wasm-bindgen-test = "0.2.38"
27+wasm-bindgen-test = "0.3.5"
examples/hello-world/README.md+1−1
@@ -9,7 +9,7 @@ See `src/lib.rs`.
99## Build
1010
1111```
12−wasm-pack build --target no-modules
12+wasm-pack build --target web
1313```
1414
1515## Serve
examples/hello-world/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Hello, World!</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_hello_world.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_hello_world_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_hello_world.js";
10+ init("./pkg/dodrio_hello_world_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/hello-world/src/lib.rs+12−14
@@ -1,24 +1,20 @@
1−use dodrio::bumpalo::Bump;
2−use dodrio::{Node, Render, Vdom};
1+use dodrio::{builder::*, bumpalo};
2+use dodrio::{Node, Render, RenderContext, Vdom};
33use wasm_bindgen::prelude::*;
44
55/// A rendering component that displays a greeting.
6−struct Hello<'who> {
6+struct Hello {
77 /// Who to greet.
8− who: &'who str,
8+ who: String,
99}
1010
1111// The `Render` implementation describes how to render a `Hello` component into
1212// HTML.
13−impl<'who> Render for Hello<'who> {
14− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
15− where
16− 'a: 'bump,
17− {
18− use dodrio::builder::*;
19− p(bump)
20− .children([text("Hello, "), text(self.who), text("!")])
21− .finish()
13+impl<'a> Render<'a> for Hello {
14+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
15+ let msg = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
16+ let msg = msg.into_bump_str();
17+ p(&cx).children([text(msg)]).finish()
2218 }
2319}
2420
@@ -33,7 +29,9 @@ pub fn run() {
3329 let body = document.body().unwrap_throw();
3430
3531 // Create a new `Hello` render component.
36− let component = Hello { who: "World" };
32+ let component = Hello {
33+ who: String::from("World"),
34+ };
3735
3836 // Create a virtual DOM and mount it and the `Hello` render component to the
3937 // `<body>`.
examples/input-form/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.15"
18+version = "0.3.32"
1919features = [
2020 "console",
2121 "Document",
@@ -29,4 +29,4 @@ features = [
2929]
3030
3131[dev-dependencies]
32−wasm-bindgen-test = "0.2.38"
32+wasm-bindgen-test = "0.3.5"
examples/input-form/README.md+1−1
@@ -10,7 +10,7 @@ See `src/lib.rs`.
1010## Build
1111
1212```
13−wasm-pack build --target no-modules
13+wasm-pack build --target web
1414```
1515
1616## Serve
examples/input-form/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Input Form</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_input_form.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_input_form_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_input_form.js";
10+ init("./pkg/dodrio_input_form_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/input-form/src/lib.rs+10−11
@@ -1,5 +1,4 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Node, Render};
1+use dodrio::{bumpalo, Node, Render, RenderContext};
32use wasm_bindgen::prelude::*;
43use wasm_bindgen::JsCast;
54
@@ -24,18 +23,18 @@ impl SayHelloTo {
2423
2524// The `Render` implementation has a text `<input>` and a `<div>` that shows a
2625// greeting to the `<input>`'s value.
27−impl Render for SayHelloTo {
28− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
29− where
30− 'a: 'bump,
31− {
26+impl<'a> Render<'a> for SayHelloTo {
27+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3228 use dodrio::builder::*;
3329
34− div(bump)
30+ div(&cx)
3531 .children([
36− input(bump)
32+ input(&cx)
3733 .attr("type", "text")
38− .attr("value", &self.who)
34+ .attr(
35+ "value",
36+ bumpalo::format!(in cx.bump, "{}", self.who).into_bump_str(),
37+ )
3938 .on("input", |root, vdom, event| {
4039 // If the event's target is our input...
4140 let input = match event
@@ -55,7 +54,7 @@ impl Render for SayHelloTo {
5554 vdom.schedule_render();
5655 })
5756 .finish(),
58− text(bumpalo::format!(in bump, "Hello, {}!", self.who).into_bump_str()),
57+ text(bumpalo::format!(in cx.bump, "Hello, {}!", self.who).into_bump_str()),
5958 ])
6059 .finish()
6160 }
examples/js-component/Cargo.toml+3−3
@@ -12,12 +12,12 @@ crate-type = ["cdylib"]
1212[dependencies]
1313dodrio = { path = "../.." }
1414dodrio-js-api = { path = "../../crates/js-api" }
15−wasm-bindgen = "0.2.38"
15+wasm-bindgen = "0.2.55"
1616console_error_panic_hook = "0.1.6"
17−js-sys = "0.3.15"
17+js-sys = "0.3.32"
1818
1919[dependencies.web-sys]
20−version = "0.3.15"
20+version = "0.3.32"
2121features = [
2222 "Document",
2323 "HtmlElement",
examples/js-component/README.md+1−1
@@ -11,7 +11,7 @@ See `src/lib.rs` for the Rust code.
1111## Build
1212
1313```
14−wasm-pack build --target no-modules
14+wasm-pack build --target web
1515```
1616
1717## Serve
examples/js-component/index.html+3−3
@@ -6,9 +6,9 @@
66 </head>
77 <body>
88 <script src="greeting.js"></script>
9− <script src="pkg/dodrio_js_component.js"></script>
10− <script>
11− wasm_bindgen("pkg/dodrio_js_component_bg.wasm");
9+ <script type="module">
10+ import init from "./pkg/dodrio_js_component.js";
11+ init("./pkg/dodrio_js_component_bg.wasm");
1212 </script>
1313 </body>
1414</html>
examples/js-component/src/lib.rs+6−10
@@ -1,5 +1,4 @@
1−use dodrio::bumpalo::Bump;
2−use dodrio::{Node, Render, Vdom};
1+use dodrio::{Node, Render, RenderContext, Vdom};
32use dodrio_js_api::JsRender;
43use js_sys::Object;
54use wasm_bindgen::prelude::*;
@@ -32,14 +31,11 @@ impl GreetingViaJs {
3231
3332/// Here's the `Render` implementation! This adds a `<p>` element and some text
3433/// around whatever the inner JS `Greeting` component renders.
35−impl Render for GreetingViaJs {
36− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
37− where
38− 'a: 'bump,
39− {
34+impl<'a> Render<'a> for GreetingViaJs {
35+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
4036 use dodrio::builder::*;
41− p(bump)
42− .children([text("JavaScript says: "), self.js.render(bump)])
37+ p(&cx)
38+ .children([text("JavaScript says: "), self.js.render(cx)])
4339 .finish()
4440 }
4541}
@@ -58,7 +54,7 @@ pub fn run() {
5854 // Create a new `GreetingViaJs` render component.
5955 let component = GreetingViaJs::new("World");
6056
61− // Create a virtual DOM and mount it and the `Hello` render component to the
57+ // Create a virtual DOM and mount it and the `GreetingViaJs` render component to the
6258 // `<body>`.
6359 let vdom = Vdom::new(body.as_ref(), component);
6460
examples/moire/Cargo.toml+8−9
@@ -8,20 +8,19 @@ edition = "2018"
88crate-type = ["cdylib"]
99
1010[dependencies]
11−cfg-if = "0.1.7"
11+cfg-if = "0.1.10"
1212dodrio = { path = "../.." }
13−futures = "0.1.25"
14−log = "0.4.6"
15−wasm-bindgen = "0.2.38"
16−wasm-bindgen-futures = "0.3.15"
13+log = "0.4.11"
14+wasm-bindgen = "0.2.55"
15+wasm-bindgen-futures = "0.4.5"
1716
1817# Optional dependencies for logging.
1918console_error_panic_hook = { optional = true, version = "0.1.6" }
20−console_log = { optional = true, version = "0.1.2" }
21−js-sys = "0.3.15"
19+console_log = { optional = true, version = "0.2.0" }
20+js-sys = "0.3.32"
2221
2322[dependencies.web-sys]
24−version = "0.3.15"
23+version = "0.3.32"
2524features = [
2625 "Document",
2726 "Element",
@@ -35,7 +34,7 @@ features = [
3534]
3635
3736[dev-dependencies]
38−wasm-bindgen-test = "0.2.38"
37+wasm-bindgen-test = "0.3.5"
3938
4039[features]
4140logging = [
examples/moire/README.md+1−1
@@ -14,7 +14,7 @@ conversion and other color utilities.
1414## Build
1515
1616```
17−wasm-pack build --target no-modules
17+wasm-pack build --target web
1818```
1919
2020## Serve
examples/moire/index.html+3−3
@@ -42,9 +42,9 @@
4242 <body>
4343 <code>FPS: <output id="fps"></code></code>
4444 <div id="scene"></div>
45− <script src="pkg/dodrio_moire.js"></script>
46− <script>
47− wasm_bindgen("pkg/dodrio_moire_bg.wasm");
45+ <script type="module">
46+ import init from "./pkg/dodrio_moire.js";
47+ init("./pkg/dodrio_moire_bg.wasm");
4848 </script>
4949 <script>
5050 let lastTime = 0
examples/moire/src/lib.rs+38−64
@@ -1,13 +1,11 @@
11mod colors;
22
33use cfg_if::cfg_if;
4−use dodrio::bumpalo::{self, Bump};
5−use dodrio::{Node, Render, Vdom};
4+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
65use std::cell::RefCell;
76use std::rc::Rc;
87use std::str;
9−use wasm_bindgen::prelude::*;
10−use wasm_bindgen::JsCast;
8+use wasm_bindgen::{prelude::*, JsCast};
119
1210/// This is the main function that is automatically invoked when the wasm module
1311/// is loaded.
@@ -24,12 +22,12 @@ pub fn run() {
2422 // Create a new `Moire` render component.
2523 let component = Moire::new();
2624
27− // Create a virtual DOM and mount it and the `Hello` render component to the
25+ // Create a virtual DOM and mount it and the `Moire` render component to the
2826 // scene.
2927 let vdom = Vdom::new(&scene, component);
3028
3129 // Kick off a loop that keeps re-rendering on every animation frame.
32− let rc: Rc<RefCell<Option<Closure<FnMut()>>>> = Rc::new(RefCell::new(None));
30+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
3331 let rc2 = rc.clone();
3432 let window2 = window.clone();
3533 let weak = vdom.weak();
@@ -44,7 +42,7 @@ pub fn run() {
4442 .unchecked_ref::<js_sys::Function>(),
4543 )
4644 .unwrap_throw();
47− }) as Box<FnMut()>);
45+ }) as Box<dyn FnMut()>);
4846 window2
4947 .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
5048 .unwrap_throw();
@@ -106,11 +104,8 @@ impl Moire {
106104 }
107105}
108106
109−impl Render for Moire {
110− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
111− where
112− 'a: 'bump,
113− {
107+impl<'a> Render<'a> for Moire {
108+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
114109 use dodrio::builder::*;
115110
116111 let elapsed = web_sys::window()
@@ -120,18 +115,18 @@ impl Render for Moire {
120115 .now();
121116 let elapsed = elapsed / 1600.0;
122117
123− main(bump)
124− .attr("style", self.moire_style(bump, elapsed))
118+ main(&cx)
119+ .attr("style", self.moire_style(cx, elapsed))
125120 .children([
126121 // The `<input>` that lets users control how many circles to
127122 // render.
128− input(bump)
123+ input(&cx)
129124 .attr("id", "circle-count")
130125 .attr("type", "range")
131126 .attr("min", "30")
132127 .attr("max", "500")
133128 .attr("value", {
134− let count = bumpalo::format!(in bump, "{}", self.count);
129+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
135130 count.into_bump_str()
136131 })
137132 .on("change", |root, _vdom, event| {
@@ -144,8 +139,8 @@ impl Render for Moire {
144139 })
145140 .finish(),
146141 // And the actual concentric circles that form the Moire patterns.
147− self.orbiting_objects(bump, elapsed),
148− self.lemniscate_objects(bump, elapsed),
142+ self.orbiting_objects(cx, elapsed),
143+ self.lemniscate_objects(cx, elapsed),
149144 ])
150145 .finish()
151146 }
@@ -154,14 +149,11 @@ impl Render for Moire {
154149/// Rendering helper methods.
155150impl Moire {
156151 /// Generate the main Moire element's inline CSS styles.
157− fn moire_style<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> &'bump str
158− where
159− 'a: 'bump,
160− {
152+ fn moire_style<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
161153 let elapsed = elapsed / 3.0;
162154 let color = colors::get_interpolated_color(|(_, bg)| bg, elapsed % 1.0);
163155 let style = bumpalo::format!(
164− in bump,
156+ in cx.bump,
165157 "background-color: rgb({}, {}, {})",
166158 color.r,
167159 color.g,
@@ -171,93 +163,75 @@ impl Moire {
171163 }
172164
173165 /// Generate the orbiting circle objects.
174− fn orbiting_objects<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> Node<'bump>
175− where
176− 'a: 'bump,
177− {
166+ fn orbiting_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
178167 const D: f64 = 200.0;
179168 let x = elapsed.sin() * D;
180169 let y = elapsed.cos() * D;
181− self.moving_object(bump, elapsed, x, y)
170+ self.moving_object(cx, elapsed, x, y)
182171 }
183172
184173 /// Generate the lemniscate circle objects that interact with the orbiting
185174 /// circle objects to form Moire patterns.
186− fn lemniscate_objects<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> Node<'bump>
187− where
188− 'a: 'bump,
189− {
175+ fn lemniscate_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
190176 const A: f64 = 150.0;
191177 let x = elapsed.sin() * A;
192178 let y = elapsed.sin() * elapsed.cos() * A;
193− self.moving_object(bump, elapsed, x, y)
179+ self.moving_object(cx, elapsed, x, y)
194180 }
195181
196− fn moving_object<'a, 'bump>(
197− &'a self,
198− bump: &'bump Bump,
182+ fn moving_object<'a>(
183+ &self,
184+ cx: &mut RenderContext<'a>,
199185 elapsed: f64,
200186 x: f64,
201187 y: f64,
202− ) -> Node<'bump>
203− where
204− 'a: 'bump,
205− {
188+ ) -> Node<'a> {
206189 use dodrio::builder::*;
207− div(bump)
190+ div(&cx)
208191 .attr("class", "object")
209− .attr("style", self.moving_object_style(bump, x, y))
210− .children([self.circle(bump, elapsed, self.count)])
192+ .attr("style", self.moving_object_style(cx, x, y))
193+ .children([self.circle(cx, elapsed, self.count)])
211194 .finish()
212195 }
213196
214197 /// Generate inline CSS styles for a moving object.
215− fn moving_object_style<'a, 'bump>(&'a self, bump: &'bump Bump, x: f64, y: f64) -> &'bump str
216− where
217− 'a: 'bump,
218− {
219− bumpalo::format!(in bump, "left: {}px; top: {}px;", x, y).into_bump_str()
198+ fn moving_object_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
199+ bumpalo::format!(in cx.bump, "left: {}px; top: {}px;", x, y).into_bump_str()
220200 }
221201
222202 /// Recursively generate `self.count` concentric circles.
223− fn circle<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64, n: u32) -> Node<'bump>
224− where
225− 'a: 'bump,
226− {
203+ fn circle<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64, n: u32) -> Node<'a> {
227204 use dodrio::builder::*;
228205
229206 let r = n * 16;
230207
231− let mut circle = div(bump)
208+ let mut circle = div(&cx)
232209 .attr("class", "circle")
233210 .attr("data-radius", {
234− let r = bumpalo::format!(in bump, "{}", r);
211+ let r = bumpalo::format!(in cx.bump, "{}", r);
235212 r.into_bump_str()
236213 })
237− .attr("style", self.circle_style(bump, elapsed, n, r));
214+ .attr("style", self.circle_style(cx, elapsed, n, r));
238215
239216 if n > 0 {
240− circle = circle.child(self.circle(bump, elapsed, n - 1));
217+ circle = circle.child(self.circle(cx, elapsed, n - 1));
241218 }
242219
243220 circle.finish()
244221 }
245222
246223 /// Generate inline CSS styles for a circle.
247− fn circle_style<'a, 'bump>(
248− &'a self,
249− bump: &'bump Bump,
224+ fn circle_style<'a>(
225+ &self,
226+ cx: &mut RenderContext<'a>,
250227 elapsed: f64,
251228 n: u32,
252229 r: u32,
253− ) -> &'bump str
254− where
255− 'a: 'bump,
256− {
230+ ) -> &'a str {
257231 let (border, alpha) = self.circle_color(elapsed, n);
258232 let margin = r / 2 + 3;
259233 let style = bumpalo::format!(
260− in bump,
234+ in cx.bump,
261235 "border-color: rgba({}, {}, {}, {});\
262236 margin-left: -{}px;\
263237 margin-top: -{}px;\
examples/sierpinski-triangle/Cargo.tomlnew file+28
@@ -0,0 +1,28 @@
1+[package]
2+name = "dodrio-sierpinski"
3+version = "0.1.0"
4+authors = ["Steven Seguin <steven.seguin@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[dependencies]
11+dodrio = { path = "../.." }
12+wasm-bindgen = "0.2.55"
13+console_error_panic_hook = "0.1.6"
14+js-sys = "0.3.32"
15+wasm-bindgen-futures = "0.4.5"
16+
17+[dependencies.web-sys]
18+version = "0.3.32"
19+features = [
20+ "Document",
21+ "HtmlElement",
22+ "Node",
23+ "Performance",
24+ "Window",
25+]
26+
27+[dev-dependencies]
28+wasm-bindgen-test = "0.3.5"
examples/sierpinski-triangle/README.mdnew file+24
@@ -0,0 +1,24 @@
1+# Sierpinski Triangle
2+
3+A Sierpinkski triangle that constantly shrinks and grows, and whose nodes have
4+a value that increments by one every second.
5+
6+Ported from the React Fiber example [here](https://github.com/claudiopro/react-fiber-vs-stack-demo).
7+
8+## Source
9+
10+See `src/lib.rs`.
11+
12+## Build
13+
14+```
15+wasm-pack build --target web
16+```
17+
18+## Serve
19+
20+Use any HTTP server, for example:
21+
22+```
23+python -m SimpleHTTPServer
24+```
examples/sierpinski-triangle/ferris.pngnew file

Binary file not shown.

examples/sierpinski-triangle/index.htmlnew file+69
@@ -0,0 +1,69 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6+ <title>Sierpinski Triangle</title>
7+ <style>
8+ .container {
9+ position: absolute;
10+ transform-origin: 0 0;
11+ left: 50%;
12+ top: 50%;
13+ width: 10px;
14+ height: 10px;
15+ background: #eee;
16+ }
17+
18+ .dot {
19+ position: absolute;
20+ background: #61dafb;
21+ font: normal 15px sans-serif;
22+ text-align: center;
23+ cursor: pointer;
24+ }
25+
26+ .dot:hover {
27+ background: url("ferris.png") no-repeat center center;
28+ z-index: 999;
29+ }
30+
31+ code {
32+ position: relative;
33+ font-size: 150%;
34+ }
35+ </style>
36+</head>
37+
38+<body>
39+ <code>FPS: <output id="fps">...</output></code>
40+ <div id="scene"></div>
41+
42+ <script type="module">
43+ import init from "./pkg/dodrio_sierpinski.js";
44+ init("./pkg/dodrio_sierpinski_bg.wasm");
45+ </script>
46+
47+ <script>
48+ let lastTime = 0;
49+ let frameCount = 0;
50+ const fps = document.getElementById('fps');
51+
52+ function updateFPS(time) {
53+ frameCount++;
54+ if (lastTime + 1000.0 <= time) {
55+ fps.textContent = frameCount;
56+ lastTime = time;
57+ frameCount = 0;
58+ }
59+ }
60+
61+ requestAnimationFrame(function loop(now) {
62+ updateFPS(now);
63+ requestAnimationFrame(loop);
64+ })
65+
66+ </script>
67+</body>
68+
69+</html>
examples/sierpinski-triangle/src/lib.rsnew file+157
@@ -0,0 +1,157 @@
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
2+use std::cell::RefCell;
3+use std::rc::Rc;
4+use wasm_bindgen::{prelude::*, JsCast};
5+use web_sys::Window;
6+
7+// Triangle container defining target size.
8+struct Container {
9+ size: f64,
10+}
11+
12+impl Container {
13+ // Construct a new container.
14+ pub fn new() -> Container {
15+ Container { size: 25.0 }
16+ }
17+
18+ // Generate the container style to fluctuate triangle width.
19+ fn container_transform<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
20+ let t = elapsed % 10.0;
21+ let scale = 0.45 + (if t > 5.0 { 10.0 - t } else { t }) / 40.0;
22+ let transform = bumpalo::format!(
23+ in cx.bump,
24+ "transform: scaleX({}) scaleY(0.7) translateZ(0.1px)",
25+ scale
26+ );
27+ transform.into_bump_str()
28+ }
29+
30+ // Generate the dot's position on the grid.
31+ fn dot_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
32+ let s = self.size * 1.3;
33+ let radius = s / 2.0;
34+ let styles = bumpalo::format!(
35+ in cx.bump,
36+ r#"
37+ width: {}px;
38+ height: {}px;
39+ left: {}px;
40+ top: {}px;
41+ border-radius: {}px;
42+ line-height: {}px;
43+ "#,
44+ s, s, x, y, radius, s
45+ );
46+ styles.into_bump_str()
47+ }
48+
49+ // Create a dot node.
50+ fn dot<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64, content: u32) -> Node<'a> {
51+ use dodrio::builder::{div, text};
52+
53+ div(&cx)
54+ .attr("class", "dot")
55+ .attr("style", self.dot_style(cx, x, y))
56+ .child(text(
57+ bumpalo::format!(in cx.bump, "{}", content).into_bump_str(),
58+ ))
59+ .finish()
60+ }
61+
62+ // Create a flattened vector of dot nodes (arranged in recursive triangles)
63+ fn triangle<'a>(
64+ &self,
65+ cx: &mut RenderContext<'a>,
66+ x: f64,
67+ y: f64,
68+ s: f64,
69+ content: u32,
70+ children: &mut bumpalo::collections::Vec<Node<'a>>,
71+ ) {
72+ if s <= self.size {
73+ children.push(self.dot(cx, x, y, content));
74+ return;
75+ }
76+
77+ let s = s / 2.0;
78+
79+ // Top of triangle
80+ self.triangle(cx, x, y - (s / 2.0), s, content, children);
81+ // Bottom-left of triangle
82+ self.triangle(cx, x - s, y + (s / 2.0), s, content, children);
83+ // Bottom-right of triangle
84+ self.triangle(cx, x + s, y + (s / 2.0), s, content, children);
85+ }
86+}
87+
88+impl<'a> Render<'a> for Container {
89+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
90+ use dodrio::builder::div;
91+
92+ let elapsed = web_sys::window()
93+ .unwrap_throw()
94+ .performance()
95+ .unwrap_throw()
96+ .now()
97+ / 1000.0;
98+
99+ let modulus = elapsed as u32 % 10;
100+
101+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
102+ self.triangle(cx, 0.0, 0.0, 1000.0, modulus, &mut children);
103+
104+ div(&cx)
105+ .attr("class", "container")
106+ .attr("style", self.container_transform(cx, elapsed))
107+ .children(children)
108+ .finish()
109+ }
110+}
111+
112+// Kick off a loop that keeps re-rendering on every animation frame.
113+fn animate(window: Window, vdom: Vdom) {
114+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
115+ let rc2 = rc.clone();
116+ let window2 = window.clone();
117+ let weak = vdom.weak();
118+ let f = Closure::wrap(Box::new(move || {
119+ weak.schedule_render();
120+ window
121+ .request_animation_frame(
122+ rc.borrow()
123+ .as_ref()
124+ .unwrap_throw()
125+ .as_ref()
126+ .unchecked_ref::<js_sys::Function>(),
127+ )
128+ .unwrap_throw();
129+ }) as Box<dyn FnMut()>);
130+ window2
131+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
132+ .unwrap_throw();
133+ *rc2.borrow_mut() = Some(f);
134+
135+ // Run the virtual DOM and its listeners forever.
136+ vdom.forget();
137+}
138+
139+#[wasm_bindgen(start)]
140+pub fn run() {
141+ // Set up the panic hook
142+ console_error_panic_hook::set_once();
143+
144+ // Get the scene element to render within
145+ let window = web_sys::window().unwrap();
146+ let document = window.document().unwrap();
147+ let scene = document.get_element_by_id("scene").unwrap();
148+
149+ // Construct a new Container component.
150+ let container = Container::new();
151+
152+ // Mount our container component to the scene div.
153+ let vdom = Vdom::new(&scene, container);
154+
155+ // Kick off animation loop
156+ animate(window, vdom);
157+}
examples/todomvc/Cargo.toml+11−12
@@ -5,22 +5,21 @@ name = "dodrio-todomvc"
55version = "0.1.0"
66
77[dependencies]
8−cfg-if = "0.1.7"
9−dodrio = { path = "../.." }
10−futures = "0.1.25"
11−js-sys = "0.3.15"
12−serde = { features = ["derive"], version = "1.0.89" }
13−serde_json = "1.0.39"
14−wasm-bindgen = "0.2.38"
15−wasm-bindgen-futures = "0.3.15"
8+cfg-if = "0.1.10"
9+dodrio = { path = "../..", features = ["serde"] }
10+js-sys = "0.3.32"
11+serde = { features = ["derive"], version = "1.0.114" }
12+serde_json = "1.0.57"
13+wasm-bindgen = "0.2.55"
14+wasm-bindgen-futures = "0.4.5"
1615
1716# Optional dependencies for logging.
1817console_error_panic_hook = { optional = true, version = "0.1.6" }
19−console_log = { optional = true, version = "0.1.2" }
20−log = { optional = true, version = "0.4.6" }
18+console_log = { optional = true, version = "0.2.0" }
19+log = { optional = true, version = "0.4.11" }
2120
2221[dependencies.web-sys]
23−version = "0.3.15"
22+version = "0.3.32"
2423features = [
2524 "Document",
2625 "Event",
@@ -35,7 +34,7 @@ features = [
3534]
3635
3736[dev-dependencies]
38−wasm-bindgen-test = "0.2.38"
37+wasm-bindgen-test = "0.3.5"
3938
4039[features]
4140logging = [
examples/todomvc/README.md+1−1
@@ -21,7 +21,7 @@ most important are:
2121## Build
2222
2323```
24−wasm-pack build --target no-modules
24+wasm-pack build --target web
2525```
2626
2727## Serve
examples/todomvc/index.html+3−3
@@ -13,9 +13,9 @@
1313 <footer class="info">
1414 <p>Double-click to edit a todo</p>
1515 </footer>
16− <script src="pkg/dodrio_todomvc.js"></script>
17− <script>
18− wasm_bindgen("pkg/dodrio_todomvc_bg.wasm");
16+ <script type="module">
17+ import init from "./pkg/dodrio_todomvc.js";
18+ init("./pkg/dodrio_todomvc_bg.wasm");
1919 </script>
2020 </body>
2121</html>
examples/todomvc/src/router.rsmode 100644 → 100755+23−21
@@ -4,10 +4,8 @@ use crate::todos::Todos;
44use crate::utils;
55use crate::visibility::Visibility;
66use dodrio::VdomWeak;
7−use futures::prelude::*;
87use std::str::FromStr;
9−use wasm_bindgen::prelude::*;
10−use wasm_bindgen::JsCast;
8+use wasm_bindgen::{prelude::*, JsCast};
119
1210/// Start the router.
1311pub fn start(vdom: VdomWeak) {
@@ -30,23 +28,27 @@ pub fn start(vdom: VdomWeak) {
3028 v
3129 });
3230
33− wasm_bindgen_futures::spawn_local(
34− vdom.with_component({
35− let vdom = vdom.clone();
36− move |root| {
37− let todos = root.unwrap_mut::<Todos>();
38− // If the todos' visibility already matches the event's
39− // visibility, then there is nothing to do (ha). If they
40− // don't match, then we need to update the todos' visibility
41− // and re-render.
42− if todos.visibility() != new_vis {
43− todos.set_visibility(new_vis);
44− vdom.schedule_render();
45− }
46− }
47− })
48− .map_err(|_| ()),
49− );
31+ wasm_bindgen_futures::spawn_local({
32+ let vdom = vdom.clone();
33+ async move {
34+ let _ = vdom
35+ .with_component({
36+ let vdom = vdom.clone();
37+ move |root| {
38+ let todos = root.unwrap_mut::<Todos>();
39+ // If the todos' visibility already matches the event's
40+ // visibility, then there is nothing to do (ha). If they
41+ // don't match, then we need to update the todos' visibility
42+ // and re-render.
43+ if todos.visibility() != new_vis {
44+ todos.set_visibility(new_vis);
45+ vdom.schedule_render();
46+ }
47+ }
48+ })
49+ .await;
50+ }
51+ });
5052 };
5153
5254 // Call it once to handle the initial `#` fragment.
@@ -57,7 +59,7 @@ pub fn start(vdom: VdomWeak) {
5759 // Note that if we ever intended to unmount our todos app, we would want to
5860 // provide a method for removing this router's event listener and cleaning
5961 // up after ourselves.
60− let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<FnMut()>);
62+ let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<dyn FnMut()>);
6163 let window = utils::window();
6264 window
6365 .add_event_listener_with_callback("hashchange", on_hash_change.as_ref().unchecked_ref())
examples/todomvc/src/todo.rs+86−32
@@ -1,14 +1,19 @@
11//! Type definition and `dodrio::Render` implementation for a single todo item.
22
33use crate::keys;
4−use dodrio::{bumpalo::Bump, Node, Render, RootRender, VdomWeak};
4+use dodrio::{Cached, Node, Render, RenderContext, RootRender, VdomWeak};
55use serde::{Deserialize, Serialize};
66use std::marker::PhantomData;
77use wasm_bindgen::{prelude::*, JsCast};
88
99/// A single todo item.
10−#[derive(Serialize, Deserialize)]
10+#[derive(Default)]
1111pub struct Todo<C> {
12+ inner: Cached<TodoInner<C>>,
13+}
14+
15+#[derive(Serialize, Deserialize)]
16+struct TodoInner<C> {
1217 id: usize,
1318 title: String,
1419 completed: bool,
@@ -48,93 +53,110 @@ impl<C> Todo<C> {
4853 let completed = false;
4954 let edits = None;
5055 Todo {
51− id,
52− title,
53− completed,
54− edits,
55− _controller: PhantomData,
56+ inner: Cached::new(TodoInner {
57+ id,
58+ title,
59+ completed,
60+ edits,
61+ _controller: PhantomData,
62+ }),
5663 }
5764 }
5865
5966 /// Set this todo item's id.
6067 pub fn set_id(&mut self, id: usize) {
61− self.id = id;
68+ if id != self.inner.id {
69+ Cached::invalidate(&self.inner);
70+ }
71+ self.inner.id = id;
6272 }
6373
6474 /// Is this `Todo` complete?
6575 pub fn is_complete(&self) -> bool {
66− self.completed
76+ self.inner.completed
6777 }
6878
6979 /// Mark the `Todo` as complete or not.
7080 pub fn set_complete(&mut self, to: bool) {
71− self.completed = to;
81+ if to != self.inner.completed {
82+ Cached::invalidate(&self.inner);
83+ }
84+ self.inner.completed = to;
7285 }
7386
7487 /// Get this todo's title.
7588 pub fn title(&self) -> &str {
76− &self.title
89+ &self.inner.title
7790 }
7891
7992 /// Set this todo item's title.
8093 pub fn set_title<S: Into<String>>(&mut self, title: S) {
81− self.title = title.into();
94+ let title = title.into();
95+ if title != self.inner.title {
96+ Cached::invalidate(&self.inner);
97+ }
98+ self.inner.title = title;
8299 }
83100
84101 /// Set the edits for this todo.
85102 pub fn set_edits<S: Into<String>>(&mut self, edits: Option<S>) {
86− self.edits = edits.map(Into::into);
103+ let edits = edits.map(Into::into);
104+ if edits != self.inner.edits {
105+ Cached::invalidate(&self.inner);
106+ }
107+ self.inner.edits = edits;
87108 }
88109
89110 /// Take this todo's edits, leaving `None` in their place.
90111 pub fn take_edits(&mut self) -> Option<String> {
91− self.edits.take()
112+ if self.inner.edits.is_some() {
113+ Cached::invalidate(&self.inner);
114+ }
115+ self.inner.edits.take()
92116 }
93117}
94118
95−impl<C: TodoActions> Render for Todo<C> {
96− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
97− where
98− 'a: 'bump,
99− {
119+impl<'a, C: TodoActions> Render<'a> for Todo<C> {
120+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
100121 use dodrio::{
101122 builder::*,
102123 bumpalo::{self, collections::String},
103124 };
104125
105− let id = self.id;
106− let title = self.edits.as_ref().unwrap_or(&self.title);
126+ let id = self.inner.id;
127+ let title = self.inner.edits.as_ref().unwrap_or(&self.inner.title);
128+ let title = bumpalo::format!(in cx.bump, "{}", title).into_bump_str();
107129
108− li(bump)
130+ li(&cx)
109131 .attr("class", {
110− let mut class = String::new_in(bump);
111− if self.completed {
132+ let mut class = String::new_in(cx.bump);
133+ if self.inner.completed {
112134 class.push_str("completed ");
113135 }
114− if self.edits.is_some() {
136+ if self.inner.edits.is_some() {
115137 class.push_str("editing");
116138 }
117139 class.into_bump_str()
118140 })
119141 .children([
120− div(bump)
142+ div(&cx)
121143 .attr("class", "view")
122144 .children([
123− input(bump)
145+ input(&cx)
124146 .attr("class", "toggle")
125147 .attr("type", "checkbox")
126− .bool_attr("checked", self.completed)
148+ .bool_attr("checked", self.inner.completed)
127149 .on("click", move |root, vdom, _event| {
128150 C::toggle_completed(root, vdom, id);
129151 })
130152 .finish(),
131− label(bump)
153+ label(&cx)
132154 .on("dblclick", move |root, vdom, _event| {
133155 C::begin_editing(root, vdom, id);
134156 })
135157 .children([text(title)])
136158 .finish(),
137− button(bump)
159+ button(&cx)
138160 .attr("class", "destroy")
139161 .on("click", move |root, vdom, _event| {
140162 C::delete(root, vdom, id);
@@ -142,13 +164,13 @@ impl<C: TodoActions> Render for Todo<C> {
142164 .finish(),
143165 ])
144166 .finish(),
145− input(bump)
167+ input(&cx)
146168 .attr("class", "edit")
147169 .attr("value", title)
148170 .attr("name", "title")
149171 .attr(
150172 "id",
151− bumpalo::format!(in bump, "todo-{}", id).into_bump_str(),
173+ bumpalo::format!(in cx.bump, "todo-{}", id).into_bump_str(),
152174 )
153175 .on("input", move |root, vdom, event| {
154176 let input = event
@@ -173,3 +195,35 @@ impl<C: TodoActions> Render for Todo<C> {
173195 .finish()
174196 }
175197}
198+
199+impl<C> Default for TodoInner<C> {
200+ fn default() -> Self {
201+ TodoInner {
202+ id: Default::default(),
203+ title: Default::default(),
204+ completed: Default::default(),
205+ edits: Default::default(),
206+ _controller: PhantomData,
207+ }
208+ }
209+}
210+
211+impl<C> serde::Serialize for Todo<C> {
212+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213+ where
214+ S: serde::Serializer,
215+ {
216+ self.inner.serialize(serializer)
217+ }
218+}
219+
220+impl<'de, C> Deserialize<'de> for Todo<C> {
221+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222+ where
223+ D: serde::Deserializer<'de>,
224+ {
225+ Ok(Todo {
226+ inner: <Cached<TodoInner<C>>>::deserialize(deserializer)?,
227+ })
228+ }
229+}
examples/todomvc/src/todos.rsmode 100644 → 100755+40−56
@@ -5,15 +5,11 @@ use crate::controller::Controller;
55use crate::todo::{Todo, TodoActions};
66use crate::visibility::Visibility;
77use crate::{keys, utils};
8−use dodrio::{
9− bumpalo::{self, Bump},
10− Node, Render, RootRender, VdomWeak,
11−};
8+use dodrio::{bumpalo, Node, Render, RenderContext, RootRender, VdomWeak};
129use serde::{Deserialize, Serialize};
1310use std::marker::PhantomData;
1411use std::mem;
15−use wasm_bindgen::prelude::*;
16−use wasm_bindgen::JsCast;
12+use wasm_bindgen::{prelude::*, JsCast};
1713
1814/// A collection of todos.
1915#[derive(Default, Serialize, Deserialize)]
@@ -32,7 +28,7 @@ pub struct Todos<C = Controller> {
3228}
3329
3430/// Actions for `Todos` that can be triggered by UI interactions.
35−pub trait TodosActions: TodoActions {
31+pub trait TodosActions: 'static + TodoActions {
3632 /// Toggle the completion state of all todo items.
3733 fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
3834
@@ -53,7 +49,7 @@ pub trait TodosActions: TodoActions {
5349impl<C> Todos<C> {
5450 /// Construct a new todos set.
5551 ///
56− /// If an existing set is available in local storage, then us that,
52+ /// If an existing set is available in local storage, then use that,
5753 /// otherwise create a new set.
5854 pub fn new() -> Self
5955 where
@@ -136,17 +132,14 @@ impl<C> Todos<C> {
136132
137133/// Rendering helpers.
138134impl<C: TodosActions> Todos<C> {
139− fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
140− where
141− 'a: 'bump,
142− {
135+ fn header<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
143136 use dodrio::builder::*;
144137
145− header(bump)
138+ header(&cx)
146139 .attr("class", "header")
147140 .children([
148− h1(bump).children([text("todos")]).finish(),
149− input(bump)
141+ h1(&cx).children([text("todos")]).finish(),
142+ input(&cx)
150143 .on("input", |root, vdom, event| {
151144 let input = event
152145 .target()
@@ -163,19 +156,19 @@ impl<C: TodosActions> Todos<C> {
163156 .attr("class", "new-todo")
164157 .attr("placeholder", "What needs to be done?")
165158 .attr("autofocus", "")
166− .attr("value", &self.draft)
159+ .attr(
160+ "value",
161+ bumpalo::format!(in cx.bump, "{}", self.draft).into_bump_str(),
162+ )
167163 .finish(),
168164 ])
169165 .finish()
170166 }
171167
172− fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
173− where
174− 'a: 'bump,
175− {
168+ fn todos_list<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
176169 use dodrio::{builder::*, bumpalo::collections::Vec};
177170
178− let mut todos = Vec::with_capacity_in(self.todos.len(), bump);
171+ let mut todos = Vec::with_capacity_in(self.todos.len(), cx.bump);
179172 todos.extend(
180173 self.todos
181174 .iter()
@@ -184,10 +177,10 @@ impl<C: TodosActions> Todos<C> {
184177 Visibility::Active => !t.is_complete(),
185178 Visibility::Completed => t.is_complete(),
186179 })
187− .map(|t| t.render(bump)),
180+ .map(|t| t.render(cx)),
188181 );
189182
190− section(bump)
183+ section(&cx)
191184 .attr("class", "main")
192185 .attr(
193186 "visibility",
@@ -198,7 +191,7 @@ impl<C: TodosActions> Todos<C> {
198191 },
199192 )
200193 .children([
201− input(bump)
194+ input(&cx)
202195 .attr("class", "toggle-all")
203196 .attr("id", "toggle-all")
204197 .attr("type", "checkbox")
@@ -208,19 +201,16 @@ impl<C: TodosActions> Todos<C> {
208201 C::toggle_all(root, vdom);
209202 })
210203 .finish(),
211− label(bump)
204+ label(&cx)
212205 .attr("for", "toggle-all")
213206 .children([text("Mark all as complete")])
214207 .finish(),
215− ul(bump).attr("class", "todo-list").children(todos).finish(),
208+ ul(&cx).attr("class", "todo-list").children(todos).finish(),
216209 ])
217210 .finish()
218211 }
219212
220− fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
221− where
222− 'a: 'bump,
223− {
213+ fn footer<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
224214 use dodrio::builder::*;
225215
226216 let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
@@ -230,36 +220,36 @@ impl<C: TodosActions> Todos<C> {
230220 } else {
231221 " items left"
232222 };
233− let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count);
223+ let incomplete_count = bumpalo::format!(in cx.bump, "{}", incomplete_count);
234224
235225 let clear_completed_text = bumpalo::format!(
236− in bump,
226+ in cx.bump,
237227 "Clear completed ({})",
238228 self.todos.iter().filter(|t| t.is_complete()).count()
239229 );
240230
241− footer(bump)
231+ footer(&cx)
242232 .attr("class", "footer")
243233 .bool_attr("hidden", self.todos.is_empty())
244234 .children([
245− span(bump)
235+ span(&cx)
246236 .attr("class", "todo-count")
247237 .children([
248− strong(bump)
238+ strong(&cx)
249239 .children([text(incomplete_count.into_bump_str())])
250240 .finish(),
251241 text(items_left),
252242 ])
253243 .finish(),
254− ul(bump)
244+ ul(&cx)
255245 .attr("class", "filters")
256246 .children([
257− self.visibility_swap(bump, "#/", Visibility::All),
258− self.visibility_swap(bump, "#/active", Visibility::Active),
259− self.visibility_swap(bump, "#/completed", Visibility::Completed),
247+ self.visibility_swap(cx, "#/", Visibility::All),
248+ self.visibility_swap(cx, "#/active", Visibility::Active),
249+ self.visibility_swap(cx, "#/completed", Visibility::Completed),
260250 ])
261251 .finish(),
262− button(bump)
252+ button(&cx)
263253 .on("click", |root, vdom, _event| {
264254 C::delete_completed(root, vdom);
265255 })
@@ -271,22 +261,19 @@ impl<C: TodosActions> Todos<C> {
271261 .finish()
272262 }
273263
274− fn visibility_swap<'a, 'bump>(
275− &'a self,
276− bump: &'bump Bump,
264+ fn visibility_swap<'a>(
265+ &self,
266+ cx: &mut RenderContext<'a>,
277267 url: &'static str,
278268 target_vis: Visibility,
279− ) -> Node<'bump>
280− where
281− 'a: 'bump,
282− {
269+ ) -> Node<'a> {
283270 use dodrio::builder::*;
284271
285− li(bump)
272+ li(&cx)
286273 .on("click", move |root, vdom, _event| {
287274 C::change_visibility(root, vdom, target_vis);
288275 })
289− .children([a(bump)
276+ .children([a(&cx)
290277 .attr("href", url)
291278 .attr(
292279 "class",
@@ -302,15 +289,12 @@ impl<C: TodosActions> Todos<C> {
302289 }
303290}
304291
305−impl<C: TodosActions> Render for Todos<C> {
306− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
307− where
308− 'a: 'bump,
309− {
292+impl<'a, C: TodosActions> Render<'a> for Todos<C> {
293+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
310294 use dodrio::builder::*;
311295
312− div(bump)
313− .children([self.header(bump), self.todos_list(bump), self.footer(bump)])
296+ div(&cx)
297+ .children([self.header(cx), self.todos_list(cx), self.footer(cx)])
314298 .finish()
315299 }
316300}
js/change-list-interpreter.jsnew file+377
@@ -0,0 +1,377 @@
1+const decoder = new TextDecoder();
2+
3+function top(stack) {
4+ return stack[stack.length - 1];
5+}
6+
7+function string(mem, pointer, length) {
8+ const buf = mem.subarray(pointer, pointer + length);
9+ return decoder.decode(buf);
10+}
11+
12+const OP_TABLE = [
13+ // 0
14+ function setText(interpreter, mem8, mem32, i) {
15+ const pointer = mem32[i++];
16+ const length = mem32[i++];
17+ const str = string(mem8, pointer, length);
18+ top(interpreter.stack).textContent = str;
19+ return i;
20+ },
21+
22+ // 1
23+ function removeSelfAndNextSiblings(interpreter, mem8, mem32, i) {
24+ const node = interpreter.stack.pop();
25+ let sibling = node.nextSibling;
26+ while (sibling) {
27+ const temp = sibling.nextSibling;
28+ sibling.remove();
29+ sibling = temp;
30+ }
31+ node.remove();
32+ return i;
33+ },
34+
35+ // 2
36+ function replaceWith(interpreter, mem8, mem32, i) {
37+ const newNode = interpreter.stack.pop();
38+ const oldNode = interpreter.stack.pop();
39+ oldNode.replaceWith(newNode);
40+ interpreter.stack.push(newNode);
41+ return i;
42+ },
43+
44+ // 3
45+ function setAttribute(interpreter, mem8, mem32, i) {
46+ const nameId = mem32[i++];
47+ const valueId = mem32[i++];
48+ const name = interpreter.getCachedString(nameId);
49+ const value = interpreter.getCachedString(valueId);
50+ const node = top(interpreter.stack);
51+ node.setAttribute(name, value);
52+
53+ // Some attributes are "volatile" and don't work through `setAttribute`.
54+ if (name === "value") {
55+ node.value = value;
56+ }
57+ if (name === "checked") {
58+ node.checked = true;
59+ }
60+ if (name === "selected") {
61+ node.selected = true;
62+ }
63+
64+ return i;
65+ },
66+
67+ // 4
68+ function removeAttribute(interpreter, mem8, mem32, i) {
69+ const nameId = mem32[i++];
70+ const name = interpreter.getCachedString(nameId);
71+ const node = top(interpreter.stack);
72+ node.removeAttribute(name);
73+
74+ // Some attributes are "volatile" and don't work through `removeAttribute`.
75+ if (name === "value") {
76+ node.value = null;
77+ }
78+ if (name === "checked") {
79+ node.checked = false;
80+ }
81+ if (name === "selected") {
82+ node.selected = false;
83+ }
84+
85+ return i;
86+ },
87+
88+ // 5
89+ function pushReverseChild(interpreter, mem8, mem32, i) {
90+ const n = mem32[i++];
91+ const parent = top(interpreter.stack);
92+ const children = parent.childNodes;
93+ const child = children[children.length - n - 1];
94+ interpreter.stack.push(child);
95+ return i;
96+ },
97+
98+ // 6
99+ function popPushChild(interpreter, mem8, mem32, i) {
100+ const n = mem32[i++];
101+ interpreter.stack.pop();
102+ const parent = top(interpreter.stack);
103+ const children = parent.childNodes;
104+ const child = children[n];
105+ interpreter.stack.push(child);
106+ return i;
107+ },
108+
109+ // 7
110+ function pop(interpreter, mem8, mem32, i) {
111+ interpreter.stack.pop();
112+ return i;
113+ },
114+
115+ // 8
116+ function appendChild(interpreter, mem8, mem32, i) {
117+ const child = interpreter.stack.pop();
118+ top(interpreter.stack).appendChild(child);
119+ return i;
120+ },
121+
122+ // 9
123+ function createTextNode(interpreter, mem8, mem32, i) {
124+ const pointer = mem32[i++];
125+ const length = mem32[i++];
126+ const text = string(mem8, pointer, length);
127+ interpreter.stack.push(document.createTextNode(text));
128+ return i;
129+ },
130+
131+ // 10
132+ function createElement(interpreter, mem8, mem32, i) {
133+ const tagNameId = mem32[i++];
134+ const tagName = interpreter.getCachedString(tagNameId);
135+ interpreter.stack.push(document.createElement(tagName));
136+ return i;
137+ },
138+
139+ // 11
140+ function newEventListener(interpreter, mem8, mem32, i) {
141+ const eventId = mem32[i++];
142+ const eventType = interpreter.getCachedString(eventId);
143+ const a = mem32[i++];
144+ const b = mem32[i++];
145+ const el = top(interpreter.stack);
146+ el.addEventListener(eventType, interpreter.eventHandler);
147+ el[`dodrio-a-${eventType}`] = a;
148+ el[`dodrio-b-${eventType}`] = b;
149+ return i;
150+ },
151+
152+ // 12
153+ function updateEventListener(interpreter, mem8, mem32, i) {
154+ const eventId = mem32[i++];
155+ const eventType = interpreter.getCachedString(eventId);
156+ const el = top(interpreter.stack);
157+ el[`dodrio-a-${eventType}`] = mem32[i++];
158+ el[`dodrio-b-${eventType}`] = mem32[i++];
159+ return i;
160+ },
161+
162+ // 13
163+ function removeEventListener(interpreter, mem8, mem32, i) {
164+ const eventId = mem32[i++];
165+ const eventType = interpreter.getCachedString(eventId);
166+ const el = top(interpreter.stack);
167+ el.removeEventListener(eventType, interpreter.eventHandler);
168+ return i;
169+ },
170+
171+ // 14
172+ function addCachedString(interpreter, mem8, mem32, i) {
173+ const pointer = mem32[i++];
174+ const length = mem32[i++];
175+ const id = mem32[i++];
176+ const str = string(mem8, pointer, length);
177+ interpreter.addCachedString(str, id);
178+ return i;
179+ },
180+
181+ // 15
182+ function dropCachedString(interpreter, mem8, mem32, i) {
183+ const id = mem32[i++];
184+ interpreter.dropCachedString(id);
185+ return i;
186+ },
187+
188+ // 16
189+ function createElementNS(interpreter, mem8, mem32, i) {
190+ const tagNameId = mem32[i++];
191+ const tagName = interpreter.getCachedString(tagNameId);
192+ const nsId = mem32[i++];
193+ const ns = interpreter.getCachedString(nsId);
194+ interpreter.stack.push(document.createElementNS(ns, tagName));
195+ return i;
196+ },
197+
198+ // 17
199+ function saveChildrenToTemporaries(interpreter, mem8, mem32, i) {
200+ let temp = mem32[i++];
201+ const start = mem32[i++];
202+ const end = mem32[i++];
203+ const parent = top(interpreter.stack);
204+ const children = parent.childNodes;
205+ for (let i = start; i < end; i++) {
206+ interpreter.temporaries[temp++] = children[i];
207+ }
208+ return i;
209+ },
210+
211+ // 18
212+ function pushChild(interpreter, mem8, mem32, i) {
213+ const parent = top(interpreter.stack);
214+ const n = mem32[i++];
215+ const child = parent.childNodes[n];
216+ interpreter.stack.push(child);
217+ return i;
218+ },
219+
220+ // 19
221+ function pushTemporary(interpreter, mem8, mem32, i) {
222+ const temp = mem32[i++];
223+ interpreter.stack.push(interpreter.temporaries[temp]);
224+ return i;
225+ },
226+
227+ // 20
228+ function insertBefore(interpreter, mem8, mem32, i) {
229+ const before = interpreter.stack.pop();
230+ const after = interpreter.stack.pop();
231+ after.parentNode.insertBefore(before, after);
232+ interpreter.stack.push(before);
233+ return i;
234+ },
235+
236+ // 21
237+ function popPushReverseChild(interpreter, mem8, mem32, i) {
238+ const n = mem32[i++];
239+ interpreter.stack.pop();
240+ const parent = top(interpreter.stack);
241+ const children = parent.childNodes;
242+ const child = children[children.length - n - 1];
243+ interpreter.stack.push(child);
244+ return i;
245+ },
246+
247+ // 22
248+ function removeChild(interpreter, mem8, mem32, i) {
249+ const n = mem32[i++];
250+ const parent = top(interpreter.stack);
251+ const child = parent.childNodes[n];
252+ child.remove();
253+ return i;
254+ },
255+
256+ // 23
257+ function setClass(interpreter, mem8, mem32, i) {
258+ const classId = mem32[i++];
259+ const className = interpreter.getCachedString(classId);
260+ top(interpreter.stack).className = className;
261+ return i;
262+ },
263+
264+ // 24
265+ function saveTemplate(interpreter, mem8, mem32, i) {
266+ const id = mem32[i++];
267+ const template = top(interpreter.stack);
268+ interpreter.saveTemplate(id, template.cloneNode(true));
269+ return i;
270+ },
271+
272+ // 25
273+ function pushTemplate(interpreter, mem8, mem32, i) {
274+ const id = mem32[i++];
275+ const template = interpreter.getTemplate(id);
276+ interpreter.stack.push(template.cloneNode(true));
277+ return i;
278+ }
279+];
280+
281+export class ChangeListInterpreter {
282+ constructor(container) {
283+ this.trampoline = null;
284+ this.container = container;
285+ this.ranges = [];
286+ this.stack = [];
287+ this.strings = new Map();
288+ this.temporaries = [];
289+ this.templates = new Map();
290+ }
291+
292+ unmount() {
293+ this.trampoline.mounted = false;
294+
295+ // Null out all of our properties just to ensure that if we mistakenly ever
296+ // call a method on this instance again, it will throw.
297+ this.trampoline = null;
298+ this.container = null;
299+ this.ranges = null;
300+ this.stack = null;
301+ this.strings = null;
302+ this.temporaries = null;
303+ this.templates = null;
304+ }
305+
306+ addChangeListRange(start, len) {
307+ this.ranges.push(start);
308+ this.ranges.push(len);
309+ }
310+
311+ applyChanges(memory) {
312+ if (this.ranges.length == 0) {
313+ return;
314+ }
315+
316+ this.stack.push(this.container.firstChild);
317+ const mem8 = new Uint8Array(memory.buffer);
318+ const mem32 = new Uint32Array(memory.buffer);
319+
320+ for (let i = 0; i < this.ranges.length; i += 2) {
321+ const start = this.ranges[i];
322+ const len = this.ranges[i + 1];
323+ this.applyChangeRange(mem8, mem32, start, len);
324+ }
325+
326+ this.ranges.length = 0;
327+ this.stack.length = 0;
328+ this.temporaries.length = 0;
329+ }
330+
331+ applyChangeRange(mem8, mem32, start, len) {
332+ const end = (start + len) / 4;
333+ for (let i = start / 4; i < end; ) {
334+ const op = mem32[i++];
335+ i = OP_TABLE[op](this, mem8, mem32, i);
336+ }
337+ }
338+
339+ addCachedString(str, id) {
340+ this.strings.set(id, str);
341+ }
342+
343+ dropCachedString(id) {
344+ this.strings.delete(id);
345+ }
346+
347+ getCachedString(id) {
348+ return this.strings.get(id);
349+ }
350+
351+ saveTemplate(id, template) {
352+ this.templates.set(id, template);
353+ }
354+
355+ getTemplate(id) {
356+ return this.templates.get(id);
357+ }
358+
359+ initEventsTrampoline(trampoline) {
360+ this.trampoline = trampoline;
361+ trampoline.mounted = true;
362+ this.eventHandler = function(event) {
363+ if (!trampoline.mounted) {
364+ throw new Error("invocation of listener after VDOM has been unmounted");
365+ }
366+
367+ // `this` always refers to the element the handler was added to.
368+ // Since we're adding the handler to all elements our content wants
369+ // to listen for events on, this ensures that we always get the right
370+ // values for `a` and `b`.
371+ const type = event.type;
372+ const a = this[`dodrio-a-${type}`];
373+ const b = this[`dodrio-b-${type}`];
374+ trampoline(event, a, b);
375+ }
376+ }
377+}
js/change-list.jsdeleted−268
@@ -1,268 +0,0 @@
1−const decoder = new TextDecoder();
2−
3−function top(stack) {
4− return stack[stack.length - 1];
5−}
6−
7−function string(mem, pointer, length) {
8− const buf = mem.subarray(pointer, pointer + length);
9− return decoder.decode(buf);
10−}
11−
12−const OP_TABLE = [
13− // 0
14− function setText(changeList, mem8, mem32, i) {
15− const pointer = mem32[i++];
16− const length = mem32[i++];
17− const str = string(mem8, pointer, length);
18− top(changeList.stack).textContent = str;
19− return i;
20− },
21−
22− // 1
23− function removeSelfAndNextSiblings(changeList, mem8, mem32, i) {
24− const node = changeList.stack.pop();
25− let sibling = node.nextSibling;
26− while (sibling) {
27− const temp = sibling.nextSibling;
28− sibling.remove();
29− sibling = temp;
30− }
31− node.remove();
32− return i;
33− },
34−
35− // 2
36− function replaceWith(changeList, mem8, mem32, i) {
37− const newNode = changeList.stack.pop();
38− const oldNode = changeList.stack.pop();
39− oldNode.replaceWith(newNode);
40− changeList.stack.push(newNode);
41− return i;
42− },
43−
44− // 3
45− function setAttribute(changeList, mem8, mem32, i) {
46− const nameId = mem32[i++];
47− const valueId = mem32[i++];
48− const name = changeList.getString(nameId);
49− const value = changeList.getString(valueId);
50− const node = top(changeList.stack);
51− node.setAttribute(name, value);
52−
53− // Some attributes are "volatile" and don't work through `setAttribute`.
54− if (name === "value") {
55− node.value = value;
56− }
57− if (name === "checked") {
58− node.checked = true;
59− }
60− if (name === "selected") {
61− node.selected = true;
62− }
63−
64− return i;
65− },
66−
67− // 4
68− function removeAttribute(changeList, mem8, mem32, i) {
69− const nameId = mem32[i++];
70− const name = changeList.getString(nameId);
71− const node = top(changeList.stack);
72− node.removeAttribute(name);
73−
74− // Some attributes are "volatile" and don't work through `removeAttribute`.
75− if (name === "value") {
76− node.value = null;
77− }
78− if (name === "checked") {
79− node.checked = false;
80− }
81− if (name === "selected") {
82− node.selected = false;
83− }
84−
85− return i;
86− },
87−
88− // 5
89− function pushFirstChild(changeList, mem8, mem32, i) {
90− changeList.stack.push(top(changeList.stack).firstChild);
91− return i;
92− },
93−
94− // 6
95− function popPushNextSibling(changeList, mem8, mem32, i) {
96− const node = changeList.stack.pop();
97− changeList.stack.push(node.nextSibling);
98− return i;
99− },
100−
101− // 7
102− function pop(changeList, mem8, mem32, i) {
103− changeList.stack.pop();
104− return i;
105− },
106−
107− // 8
108− function appendChild(changeList, mem8, mem32, i) {
109− const child = changeList.stack.pop();
110− top(changeList.stack).appendChild(child);
111− return i;
112− },
113−
114− // 9
115− function createTextNode(changeList, mem8, mem32, i) {
116− const pointer = mem32[i++];
117− const length = mem32[i++];
118− const text = string(mem8, pointer, length);
119− changeList.stack.push(document.createTextNode(text));
120− return i;
121− },
122−
123− // 10
124− function createElement(changeList, mem8, mem32, i) {
125− const tagNameId = mem32[i++];
126− const tagName = changeList.getString(tagNameId);
127− changeList.stack.push(document.createElement(tagName));
128− return i;
129− },
130−
131− // 11
132− function newEventListener(changeList, mem8, mem32, i) {
133− const eventId = mem32[i++];
134− const eventType = changeList.getString(eventId);
135− const a = mem32[i++];
136− const b = mem32[i++];
137− const el = top(changeList.stack);
138− el.addEventListener(eventType, changeList.eventHandler);
139− el[`dodrio-a-${eventType}`] = a;
140− el[`dodrio-b-${eventType}`] = b;
141− return i;
142− },
143−
144− // 12
145− function updateEventListener(changeList, mem8, mem32, i) {
146− const eventId = mem32[i++];
147− const eventType = changeList.getString(eventId);
148− const el = top(changeList.stack);
149− el[`dodrio-a-${eventType}`] = mem32[i++];
150− el[`dodrio-b-${eventType}`] = mem32[i++];
151− return i;
152− },
153−
154− // 13
155− function removeEventListener(changeList, mem8, mem32, i) {
156− const eventId = mem32[i++];
157− const eventType = changeList.getString(eventId);
158− const el = top(changeList.stack);
159− el.removeEventListener(eventType, changeList.eventHandler);
160− return i;
161− },
162−
163− // 14
164− function addString(changeList, mem8, mem32, i) {
165− const pointer = mem32[i++];
166− const length = mem32[i++];
167− const id = mem32[i++];
168− const str = string(mem8, pointer, length);
169− changeList.addString(str, id);
170− return i;
171− },
172−
173− // 15
174− function dropString(changeList, mem8, mem32, i) {
175− const id = mem32[i++];
176− changeList.dropString(id);
177− return i;
178− }
179−];
180−
181−// export
182−class ChangeList {
183− constructor(container) {
184− this.trampoline = null;
185− this.container = container;
186− this.ranges = [];
187− this.stack = [];
188− this.strings = new Map();
189− }
190−
191− unmount() {
192− this.trampoline.mounted = false;
193−
194− // Null out all of our properties just to ensure that if we mistakenly ever
195− // call a method on this instance again, it will throw.
196− this.trampoline = null;
197− this.container = null;
198− this.ranges = null;
199− this.stack = null;
200− this.strings = null;
201− }
202−
203− addChangeListRange(start, len) {
204− this.ranges.push(start);
205− this.ranges.push(len);
206− }
207−
208− applyChanges(memory) {
209− if (this.ranges.length == 0) {
210− return;
211− }
212−
213− this.stack.push(this.container.firstChild);
214− const mem8 = new Uint8Array(memory.buffer);
215− const mem32 = new Uint32Array(memory.buffer);
216−
217− for (let i = 0; i < this.ranges.length; i += 2) {
218− const start = this.ranges[i];
219− const len = this.ranges[i + 1];
220− this.applyChangeRange(mem8, mem32, start, len);
221− }
222−
223− this.ranges.length = 0;
224− this.stack.length = 0;
225− }
226−
227− applyChangeRange(mem8, mem32, start, len) {
228− const end = (start + len) / 4;
229− for (let i = start / 4; i < end; ) {
230− const op = mem32[i++];
231− i = OP_TABLE[op](this, mem8, mem32, i);
232− }
233− }
234−
235− addString(str, id) {
236− this.strings.set(id, str);
237− }
238−
239− dropString(id) {
240− this.strings.delete(id);
241− }
242−
243− getString(id) {
244− return this.strings.get(id);
245− }
246−
247− initEventsTrampoline(trampoline) {
248− this.trampoline = trampoline;
249− trampoline.mounted = true;
250− this.eventHandler = function(event) {
251− if (!trampoline.mounted) {
252− throw new Error("invocation of listener after VDOM has been unmounted");
253− }
254−
255− // `this` always refers to the element the handler was added to.
256− // Since we're adding the handler to all elements our content wants
257− // to listen for events on, this ensures that we always get the right
258− // values for `a` and `b`.
259− const type = event.type;
260− const a = this[`dodrio-a-${type}`];
261− const b = this[`dodrio-b-${type}`];
262− trampoline(event, a, b);
263− }
264− }
265−}
266−window.ChangeList = ChangeList;
267−
268−//# sourceURL=dodrio/change-list.js
js/package-lock.jsondeleted−5
@@ -1,5 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "lockfileVersion": 1
5−}
js/package.jsondeleted−11
@@ -1,11 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "description": "Dodrio",
5− "main": "dodrio.js",
6− "scripts": {
7− "test": "echo \"Error: no test specified\" && exit 1"
8− },
9− "author": "Nick Fitzgerald <fitzgen@gmail.com>",
10− "license": "MPL-2.0"
11−}
js/strace.jsnew file+80
@@ -0,0 +1,80 @@
1+export function initStrace() {
2+ instrumentAll(Element.prototype);
3+ instrumentAll(Document.prototype);
4+ instrumentAll(Node.prototype);
5+ instrumentAll(EventTarget.prototype);
6+}
7+
8+const counts = new Map;
9+counts.total = 0;
10+
11+let timer = null;
12+
13+function log(ctorName, kind, key) {
14+ counts.total += 1;
15+
16+ const entry = `${kind} ${ctorName}#${key}`
17+ const count = counts.get(entry);
18+ if (count === undefined) {
19+ counts.set(entry, 1);
20+ } else {
21+ counts.set(entry, count + 1);
22+ }
23+
24+ if (timer !== null) {
25+ clearTimeout(timer);
26+ }
27+ timer = setTimeout(dumpAndReset, 100);
28+}
29+
30+function dumpAndReset() {
31+ const data = [...counts]
32+ .sort((a, b) => b[1] - a[1])
33+ .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
34+
35+ data.push({ "DOM Method": "<total>", "Count": counts.total });
36+
37+ console.table(data, ["DOM Method", "Count"]);
38+
39+ counts.clear();
40+ counts.total = 0;
41+}
42+
43+function instrument(proto, key, desc) {
44+ const ctorName = proto.constructor.name;
45+
46+ if (typeof desc.value == "function") {
47+ const f = desc.value;
48+ desc.value = function (...args) {
49+ log(ctorName, "call", key);
50+ return f.apply(this, args);
51+ };
52+ }
53+
54+ if (typeof desc.get == "function") {
55+ const get = desc.get;
56+ desc.get = function (...args) {
57+ log(ctorName, "get", key);
58+ return get.apply(this, args);
59+ };
60+ }
61+
62+ if (typeof desc.set == "function") {
63+ const set = desc.set;
64+ desc.set = function (...args) {
65+ log(ctorName, "set", key);
66+ return set.apply(this, args);
67+ };
68+ }
69+
70+ Object.defineProperty(proto, key, desc);
71+}
72+
73+function instrumentAll(proto) {
74+ for (const key of Object.keys(proto)) {
75+ const desc = Object.getOwnPropertyDescriptor(proto, key);
76+ if (desc.configurable) {
77+ instrument(proto, key, desc);
78+ }
79+ }
80+}
publish.shnew file+11
@@ -0,0 +1,11 @@
1+#!/usr/bin/env bash
2+
3+set -ex
4+
5+cd "$(dirname "$0")"
6+
7+cd crates/js-api
8+cargo publish --no-verify $PUBLISH_FLAGS
9+cd ../..
10+
11+cargo publish $PUBLISH_FLAGS
src/builder.rs+161−6
@@ -1,6 +1,6 @@
11//! Helpers for building virtual DOM nodes.
22
3−use crate::{Attribute, Listener, Node, RootRender, VdomWeak};
3+use crate::{node::NodeKey, Attribute, Listener, Node, RootRender, VdomWeak};
44use bumpalo::Bump;
55
66/// A virtual DOM element builder.
@@ -16,10 +16,12 @@ where
1616 Children: 'a + AsRef<[Node<'a>]>,
1717{
1818 bump: &'a Bump,
19+ key: NodeKey,
1920 tag_name: &'a str,
2021 listeners: Listeners,
2122 attributes: Attributes,
2223 children: Children,
24+ namespace: Option<&'a str>,
2325}
2426
2527impl<'a>
@@ -53,13 +55,19 @@ impl<'a>
5355 /// let my_element_builder = ElementBuilder::new(&b, tag_name);
5456 /// # fn flip_coin() -> bool { true }
5557 /// ```
56− pub fn new(bump: &'a Bump, tag_name: &'a str) -> Self {
58+ pub fn new<B>(bump: B, tag_name: &'a str) -> Self
59+ where
60+ B: Into<&'a Bump>,
61+ {
62+ let bump = bump.into();
5763 ElementBuilder {
5864 bump,
65+ key: NodeKey::NONE,
5966 tag_name,
6067 listeners: bumpalo::collections::Vec::new_in(bump),
6168 attributes: bumpalo::collections::Vec::new_in(bump),
6269 children: bumpalo::collections::Vec::new_in(bump),
70+ namespace: None,
6371 }
6472 }
6573}
@@ -104,10 +112,12 @@ where
104112 {
105113 ElementBuilder {
106114 bump: self.bump,
115+ key: self.key,
107116 tag_name: self.tag_name,
108117 listeners,
109118 attributes: self.attributes,
110119 children: self.children,
120+ namespace: self.namespace,
111121 }
112122 }
113123
@@ -141,10 +151,12 @@ where
141151 {
142152 ElementBuilder {
143153 bump: self.bump,
154+ key: self.key,
144155 tag_name: self.tag_name,
145156 listeners: self.listeners,
146157 attributes,
147158 children: self.children,
159+ namespace: self.namespace,
148160 }
149161 }
150162
@@ -178,13 +190,81 @@ where
178190 {
179191 ElementBuilder {
180192 bump: self.bump,
193+ key: self.key,
181194 tag_name: self.tag_name,
182195 listeners: self.listeners,
183196 attributes: self.attributes,
184197 children,
198+ namespace: self.namespace,
199+ }
200+ }
201+
202+ /// Set the namespace for this element.
203+ ///
204+ /// # Example
205+ ///
206+ /// ```no_run
207+ /// use dodrio::{builder::*, bumpalo::Bump};
208+ ///
209+ /// let b = Bump::new();
210+ ///
211+ /// // Create a `<td>` tag with an xhtml namespace
212+ /// let my_td = td(&b)
213+ /// .namespace(Some("http://www.w3.org/1999/xhtml"))
214+ /// .finish();
215+ /// ```
216+ #[inline]
217+ pub fn namespace(self, namespace: Option<&'a str>) -> Self {
218+ ElementBuilder {
219+ bump: self.bump,
220+ key: self.key,
221+ tag_name: self.tag_name,
222+ listeners: self.listeners,
223+ attributes: self.attributes,
224+ children: self.children,
225+ namespace,
185226 }
186227 }
187228
229+ /// Set this element's key.
230+ ///
231+ /// When diffing sets of siblings, if an old sibling and new sibling share a
232+ /// key, then they will always reuse the same physical DOM node. This is
233+ /// important when using CSS animations, web components, third party JS, or
234+ /// anything else that makes the diffing implementation observable.
235+ ///
236+ /// Do not use keys if such a scenario does not apply. Keyed diffing is
237+ /// generally more expensive than not, since it is putting greater
238+ /// constraints on the diffing algorithm.
239+ ///
240+ /// # Invariants You Must Uphold
241+ ///
242+ /// The key may not be `u32::MAX`, which is a reserved key value.
243+ ///
244+ /// Keys must be unique among siblings.
245+ ///
246+ /// All sibling nodes must be keyed, or they must all not be keyed. You may
247+ /// not mix keyed and unkeyed siblings.
248+ ///
249+ /// # Example
250+ ///
251+ /// ```no_run
252+ /// use dodrio::{builder::*, bumpalo::Bump};
253+ ///
254+ /// let b = Bump::new();
255+ ///
256+ /// let my_li = li(&b)
257+ /// .key(1337)
258+ /// .finish();
259+ /// ```
260+ #[inline]
261+ pub fn key(mut self, key: u32) -> Self {
262+ use std::u32;
263+ debug_assert!(key != u32::MAX);
264+ self.key = NodeKey(key);
265+ self
266+ }
267+
188268 /// Create the virtual DOM node described by this builder.
189269 ///
190270 /// # Example
@@ -202,12 +282,23 @@ where
202282 /// ```
203283 #[inline]
204284 pub fn finish(self) -> Node<'a> {
285+ let children: &'a Children = self.bump.alloc(self.children);
286+ let children: &'a [Node<'a>] = children.as_ref();
287+
288+ let listeners: &'a Listeners = self.bump.alloc(self.listeners);
289+ let listeners: &'a [Listener<'a>] = listeners.as_ref();
290+
291+ let attributes: &'a Attributes = self.bump.alloc(self.attributes);
292+ let attributes: &'a [Attribute<'a>] = attributes.as_ref();
293+
205294 Node::element(
206295 self.bump,
296+ self.key,
207297 self.tag_name,
208− self.listeners,
209− self.attributes,
210− self.children,
298+ listeners,
299+ attributes,
300+ children,
301+ self.namespace,
211302 )
212303 }
213304}
@@ -342,6 +433,28 @@ macro_rules! builder_constructors {
342433 ( $(
343434 $(#[$attr:meta])*
344435 $name:ident;
436+ )* ) => {
437+ $(
438+ $(#[$attr])*
439+ #[inline]
440+ pub fn $name<'a, B>(
441+ bump: B,
442+ ) -> ElementBuilder<
443+ 'a,
444+ bumpalo::collections::Vec<'a, Listener<'a>>,
445+ bumpalo::collections::Vec<'a, Attribute<'a>>,
446+ bumpalo::collections::Vec<'a, Node<'a>>,
447+ >
448+ where
449+ B: Into<&'a Bump>
450+ {
451+ ElementBuilder::new(bump, stringify!($name))
452+ }
453+ )*
454+ };
455+ ( $(
456+ $(#[$attr:meta])*
457+ $name:ident <> $namespace:tt;
345458 )* ) => {
346459 $(
347460 $(#[$attr])*
@@ -354,7 +467,8 @@ macro_rules! builder_constructors {
354467 bumpalo::collections::Vec<'a, Attribute<'a>>,
355468 bumpalo::collections::Vec<'a, Node<'a>>,
356469 > {
357− ElementBuilder::new(bump, stringify!($name))
470+ let builder = ElementBuilder::new(bump, stringify!($name));
471+ builder.namespace(Some($namespace))
358472 }
359473 )*
360474 }
@@ -861,6 +975,47 @@ builder_constructors! {
861975 template;
862976}
863977
978+builder_constructors! {
979+ // SVG components
980+
981+ /// Build a
982+ /// [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
983+ /// element.
984+ svg <> "http://www.w3.org/2000/svg" ;
985+ /// Build a
986+ /// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
987+ /// element.
988+ path <> "http://www.w3.org/2000/svg";
989+ /// Build a
990+ /// [`<circle>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
991+ /// element.
992+ circle <> "http://www.w3.org/2000/svg";
993+ /// Build a
994+ /// [`<ellipse>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse)
995+ /// element.
996+ ellipse <> "http://www.w3.org/2000/svg";
997+ /// Build a
998+ /// [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
999+ /// element.
1000+ line <> "http://www.w3.org/2000/svg";
1001+ /// Build a
1002+ /// [`<polygon>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
1003+ /// element.
1004+ polygon <> "http://www.w3.org/2000/svg";
1005+ /// Build a
1006+ /// [`<polyline>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
1007+ /// element.
1008+ polyline <> "http://www.w3.org/2000/svg";
1009+ /// Build a
1010+ /// [`<rect>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
1011+ /// element.
1012+ rect <> "http://www.w3.org/2000/svg";
1013+ /// Build a
1014+ /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
1015+ /// element.
1016+ image <> "http://www.w3.org/2000/svg";
1017+}
1018+
8641019/// Construct a text node.
8651020///
8661021/// This is `dodrio`'s virtual DOM equivalent of `document.createTextNode`.
src/cached.rs+139−73
@@ -1,46 +1,52 @@
1−use crate::{Node, Render};
2−use bumpalo::Bump;
3−use std::cell::RefCell;
4−use std::mem;
1+use crate::{
2+ cached_set::CachedSet,
3+ node::{CachedNode, NodeKey},
4+ Node, Render, RenderContext,
5+};
6+use std::any::TypeId;
7+use std::cell::Cell;
58use std::ops::{Deref, DerefMut};
69
7−// TODO: This implementation will only cache the rendering (generation of the
8−// virtual DOM) but not the diffing of the cached subtree. We could skip diffing
9−// for cached tree by adding `fn is_cached(&self) -> bool` to `Node` that we can
10−// check during diffing. This comes at the cost of bloating `Node`'s size (since
11−// we don't have any padding to sneak an extra `bool` field into). We should
12−// investigate whether it is worth adding this or not.
13−
1410/// A renderable that supports caching for when rendering is expensive but can
1511/// generate the same DOM tree.
16−#[derive(Debug)]
17−pub struct Cached<R> {
12+///
13+/// # Example
14+///
15+/// The [TodoMVC
16+/// example](https://github.com/fitzgen/dodrio/blob/master/examples/todomvc/src/todo.rs)
17+/// caches individual todo items.
18+#[derive(Clone, Debug, Default)]
19+pub struct Cached<R>
20+where
21+ R: Default,
22+{
1823 inner: R,
19− bump: bumpalo::Bump,
20− // Actually a self-reference into `self.bump`. Safe because we ensure that
21− // whenever we hand out a cached node, we use a lifetime that cannot outlive
22− // its owning `Cached<R>`.
23− cached: RefCell<Option<Node<'static>>>,
24+ cached: Cell<Option<CachedNode>>,
25+}
26+
27+pub_unstable_internal! {
28+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
29+ pub(crate) struct TemplateId(TypeId);
2430}
2531
26−impl<R> Cached<R> {
32+impl<R> Cached<R>
33+where
34+ R: Default,
35+{
2736 /// Construct a new `Cached<R>` of an inner `R`.
2837 ///
2938 /// # Example
3039 ///
3140 /// ```
32− /// use bumpalo::Bump;
33− /// use dodrio::{Cached, Node, Render};
41+ /// use dodrio::{Cached, Node, Render, RenderContext};
3442 ///
43+ /// #[derive(Default)]
3544 /// pub struct Counter {
3645 /// count: u32,
3746 /// }
3847 ///
39− /// impl Render for Counter {
40− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
41− /// where
42− /// 'a: 'bump
43− /// {
48+ /// impl<'a> Render<'a> for Counter {
49+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
4450 /// // ...
4551 /// # unimplemented!()
4652 /// }
@@ -54,13 +60,8 @@ impl<R> Cached<R> {
5460 /// ```
5561 #[inline]
5662 pub fn new(inner: R) -> Cached<R> {
57− let bump = Bump::new();
58− let cached = RefCell::new(None);
59− Cached {
60− inner,
61− bump,
62− cached,
63− }
63+ let cached = Cell::new(None);
64+ Cached { inner, cached }
6465 }
6566
6667 /// Invalidate the cached rendering.
@@ -72,26 +73,24 @@ impl<R> Cached<R> {
7273 /// # Example
7374 ///
7475 /// The `Cached<Hello>` component must have its cache invalidated whenever
75− /// the `who` string is changed, or else the cached rendering whill keep
76+ /// the `who` string is changed, or else the cached rendering will keep
7677 /// displaying greetings to old `who`s.
7778 ///
7879 /// ```
79− /// use bumpalo::Bump;
80− /// use dodrio::{Cached, Node, Render};
80+ /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
8181 ///
8282 /// /// A component that renders to "<p>Hello, {who}!</p>"
83+ /// #[derive(Default)]
8384 /// pub struct Hello {
8485 /// who: String
8586 /// }
8687 ///
87− /// impl Render for Hello {
88− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
89− /// where
90− /// 'a: 'bump,
91− /// {
88+ /// impl<'a> Render<'a> for Hello {
89+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
9290 /// use dodrio::builder::*;
93− /// p(bump)
94− /// .children([text("Hello, "), text(&self.who), text("!")])
91+ /// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
92+ /// p(&cx)
93+ /// .children([text(greeting.into_bump_str())])
9594 /// .finish()
9695 /// }
9796 /// }
@@ -104,18 +103,30 @@ impl<R> Cached<R> {
104103 /// }
105104 /// ```
106105 #[inline]
107− pub fn invalidate(cached: &mut Self) {
108− *cached.cached.borrow_mut() = None;
106+ pub fn invalidate(cached: &Self) {
107+ cached.cached.set(None);
109108 }
110109
111110 /// Convert a `Cached<R>` back into a plain `R`.
112111 #[inline]
113− pub fn into_inner(cached: Cached<R>) -> R {
112+ pub fn into_inner(cached: Self) -> R {
114113 cached.inner
115114 }
116115}
117116
118−impl<R> Deref for Cached<R> {
117+impl<R> Cached<R>
118+where
119+ R: 'static + Default,
120+{
121+ pub(crate) fn template_id() -> TemplateId {
122+ TemplateId(TypeId::of::<R>())
123+ }
124+}
125+
126+impl<R> Deref for Cached<R>
127+where
128+ R: Default,
129+{
119130 type Target = R;
120131
121132 fn deref(&self) -> &R {
@@ -123,41 +134,96 @@ impl<R> Deref for Cached<R> {
123134 }
124135}
125136
126−impl<R> DerefMut for Cached<R> {
137+impl<R> DerefMut for Cached<R>
138+where
139+ R: Default,
140+{
127141 fn deref_mut(&mut self) -> &mut R {
128142 &mut self.inner
129143 }
130144}
131145
132−unsafe fn extend_node_lifetime<'a>(node: Node<'a>) -> Node<'static> {
133− mem::transmute(node)
146+impl<'a, R> Render<'a> for Cached<R>
147+where
148+ R: 'static + Default + for<'b> Render<'b>,
149+{
150+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
151+ let template = cx.template::<R>();
152+ let cached = match self.cached.get() {
153+ // This does-the-cache-contain-this-id check is necessary because
154+ // the same `Cached<R>` instance can be rendered into vdom A, which
155+ // will save the results into A's cached set and yield id X. Then,
156+ // it can be rendered *again* into a second vdom B, and B's cached
157+ // set does not have the saved results for id X. If we didn't have
158+ // this check, instead of a cache miss, we would have a panic.
159+ //
160+ // This scenario should basically never happen in the real world,
161+ // however. If we ever find that it is worth sharing a cached render
162+ // component between multiple vdoms, and want to avoid these
163+ // "unnecessary" cache misses, we can do this:
164+ //
165+ // * Make each `Cached<R>` have an instance id and generation
166+ // counter.
167+ //
168+ // * On invalidation, bump the generation counter.
169+ //
170+ // * Add a generation member to the `CacheEntry`.
171+ //
172+ // * Each vdom maintains a map from `Cached<R>` instance id to
173+ // `CacheEntry`s.
174+ //
175+ // * We only re-use the cached results if the `Cached<R>`'s
176+ // generation counter matches the entry in the vdom's cached set.
177+ //
178+ // This is all do-able but is a bit more book keeping than we really
179+ // want to do unless it is well motivated.
180+ Some(cached)
181+ if {
182+ let cached_set = cx.cached_set.borrow();
183+ cached_set.contains(cached.id)
184+ } =>
185+ {
186+ cached
187+ }
188+ _ => {
189+ let mut key = NodeKey::NONE;
190+ let id = CachedSet::insert(cx, false, template, |nested_cx| {
191+ let node = self.inner.render(nested_cx);
192+ key = node.key();
193+ node
194+ });
195+ let cached = CachedNode { id, key };
196+ self.cached.set(Some(cached));
197+ cached
198+ }
199+ };
200+
201+ cached.into()
202+ }
134203}
135204
136−impl<R: Render> Render for Cached<R> {
137− fn render<'a, 'bump>(&'a self, _: &'bump Bump) -> Node<'bump>
205+#[cfg(feature = "serde")]
206+impl<R> serde::Serialize for Cached<R>
207+where
208+ R: Default + serde::Serialize,
209+{
210+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138211 where
139− 'a: 'bump,
212+ S: serde::Serializer,
140213 {
141− let mut cached = self.cached.borrow_mut();
142−
143− if let Some(cached) = cached.as_ref() {
144− // The cached node is actually a self-reference, so it has the `'a`
145− // lifetime.
146− let cached: Node<'a> = cached.clone();
147−
148− // But the `'a` lifetime outlives `'bump`, so we can safely convert
149− // it to the narrower `'bump` lifetime.
150− let cached: Node<'bump> = cached;
151−
152− // Return the cached rendering!
153− return cached;
154− }
214+ self.inner.serialize(serializer)
215+ }
216+}
155217
156− // We don't have a cached node. Render into our own `Bump`, cache it for
157− // future renders, and return it. Same lifetimes as above.
158− let node: Node<'a> = self.inner.render(&self.bump);
159− *cached = Some(unsafe { extend_node_lifetime(node.clone()) });
160− let node: Node<'bump> = node;
161− node
218+#[cfg(feature = "serde")]
219+impl<'de, R> serde::Deserialize<'de> for Cached<R>
220+where
221+ R: Default + serde::Deserialize<'de>,
222+{
223+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224+ where
225+ D: serde::Deserializer<'de>,
226+ {
227+ R::deserialize(deserializer).map(Cached::new)
162228 }
163229}
src/cached_set.rsnew file+186
@@ -0,0 +1,186 @@
1+use crate::{
2+ events::EventsRegistry,
3+ node::{Node, NodeKind},
4+ render_context::RenderContext,
5+};
6+use bumpalo::Bump;
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::sync::atomic::{AtomicUsize, Ordering};
9+use std::u32;
10+use wasm_bindgen::prelude::*;
11+
12+static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
13+
14+pub_unstable_internal! {
15+ #[derive(Debug, Default)]
16+ pub(crate) struct CachedSet {
17+ items: FxHashMap<CacheId, CacheEntry>,
18+ }
19+}
20+
21+pub_unstable_internal! {
22+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23+ pub(crate) struct CacheId(u32);
24+}
25+
26+#[derive(Debug)]
27+pub(crate) struct CacheEntry {
28+ bump: Bump,
29+
30+ // Actually a reference into `bump`.
31+ node: *const Node<'static>,
32+
33+ // All the other cached entries that `node` references (including transitive
34+ // edges). By eagerly following transitive edges and recording them the
35+ // once, we don't have to repeat the tracing every time we want to garbage
36+ // collect cache entries.
37+ edges: FxHashSet<CacheId>,
38+
39+ // The template for this cached virtual subtree.
40+ //
41+ // When a cache entry has a template, that means that the `ChangeList` will
42+ // lazily build a physical DOM subtree based on the template, and when we
43+ // create new versions of this cached result, we clone the template's
44+ // physical DOM subtree and then modify it, rather than building the cached
45+ // result up from scratch. This is a nice win because we bounce between JS
46+ // and DOM methods less often, and get the C++ DOM implementation to do most
47+ // of the subtree construction for us.
48+ template: Option<CacheId>,
49+
50+ // Whether this entry should never be garbage collected. Typically only
51+ // templates are pinned.
52+ pinned: bool,
53+}
54+
55+impl From<CacheId> for u32 {
56+ #[inline]
57+ fn from(id: CacheId) -> u32 {
58+ id.0
59+ }
60+}
61+
62+impl CachedSet {
63+ pub(crate) fn new_roots_set(&self) -> FxHashSet<CacheId> {
64+ let mut roots = FxHashSet::default();
65+ roots.reserve(self.items.len());
66+ roots
67+ }
68+
69+ pub(crate) fn gc(&mut self, registry: &mut EventsRegistry, roots: FxHashSet<CacheId>) {
70+ let mut marked = FxHashSet::default();
71+ marked.reserve(self.items.len());
72+
73+ for root in roots {
74+ if marked.insert(root) {
75+ let entry = self
76+ .items
77+ .get(&root)
78+ .expect_throw("CachedSet::gc: should have root in cached set");
79+
80+ // NB: no need to recurse here because `edges` already contains
81+ // transitive edges!
82+ marked.extend(entry.edges.iter().cloned());
83+ }
84+ }
85+
86+ self.items.retain(|id, entry| {
87+ let keep = entry.pinned || marked.contains(id);
88+ if !keep {
89+ let node: &Node = unsafe { &*entry.node };
90+ registry.remove_subtree(node);
91+ }
92+ keep
93+ });
94+ }
95+
96+ // Trace all the transitive edges to other cached entries that the given
97+ // node has.
98+ fn trace(&self, node: &Node) -> FxHashSet<CacheId> {
99+ let mut edges = FxHashSet::default();
100+ self.trace_recursive(&mut edges, node);
101+ edges
102+ }
103+
104+ fn trace_recursive(&self, edges: &mut FxHashSet<CacheId>, node: &Node) {
105+ match &node.kind {
106+ NodeKind::Text(_) => return,
107+ NodeKind::Cached(c) => {
108+ debug_assert!(self.items.contains_key(&c.id));
109+ edges.insert(c.id);
110+ edges.extend(
111+ self.items
112+ .get(&c.id)
113+ .expect_throw("CachedSet::trace: should have c.id in cached set")
114+ .edges
115+ .iter()
116+ .cloned(),
117+ );
118+ }
119+ NodeKind::Element(el) => {
120+ for child in el.children {
121+ self.trace_recursive(edges, child);
122+ }
123+ }
124+ }
125+ }
126+
127+ fn next_id(&mut self) -> CacheId {
128+ let next = ID_COUNTER.fetch_add(1, Ordering::AcqRel) as u32;
129+ let next = if next == u32::MAX { None } else { Some(next) };
130+ CacheId(next.expect_throw("ID_COUNTER overflowed"))
131+ }
132+
133+ pub(crate) fn insert<F>(
134+ cx: &mut RenderContext,
135+ pinned: bool,
136+ template: Option<CacheId>,
137+ f: F,
138+ ) -> CacheId
139+ where
140+ F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
141+ {
142+ let set = cx.cached_set;
143+ let bump = Bump::new();
144+ let (node, edges) = {
145+ let mut nested_cx = RenderContext::new(&bump, cx.cached_set, cx.templates);
146+ let node = f(&mut nested_cx);
147+ let node = bump.alloc(node);
148+ let edges = {
149+ let set = set.borrow();
150+ set.trace(node)
151+ };
152+ (
153+ node as *mut Node<'_> as usize as *const Node<'static>,
154+ edges,
155+ )
156+ };
157+
158+ let entry = CacheEntry {
159+ bump,
160+ node,
161+ edges,
162+ template,
163+ pinned,
164+ };
165+
166+ let mut set = set.borrow_mut();
167+ let id = set.next_id();
168+ set.items.insert(id, entry);
169+ id
170+ }
171+
172+ /// Does the cached set contain a cached node with the given id?
173+ pub fn contains(&self, id: CacheId) -> bool {
174+ self.items.contains_key(&id)
175+ }
176+
177+ /// Get the cached node and its template (if any) for the given cache id.
178+ pub fn get(&self, id: CacheId) -> (&Node, Option<CacheId>) {
179+ let entry = self
180+ .items
181+ .get(&id)
182+ .expect_throw("CachedSet::get: should have id in set");
183+ let node: &Node = unsafe { &*entry.node };
184+ (node, entry.template)
185+ }
186+}
src/change_list.rsdeleted−426
@@ -1,426 +0,0 @@
1−use crate::Listener;
2−use bumpalo::Bump;
3−use fxhash::FxHashMap;
4−use std::fmt;
5−use std::sync::Once;
6−use wasm_bindgen::prelude::*;
7−
8−pub mod js {
9− use wasm_bindgen::prelude::*;
10−
11− #[wasm_bindgen]
12− extern "C" {
13− #[derive(Clone, Debug)]
14− pub type ChangeList;
15−
16− #[wasm_bindgen(constructor)]
17− pub fn new(container: &web_sys::Node) -> ChangeList;
18−
19− #[wasm_bindgen(structural, method)]
20− pub fn unmount(this: &ChangeList);
21−
22− #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
23− pub fn add_change_list_range(this: &ChangeList, start: usize, len: usize);
24−
25− #[wasm_bindgen(structural, method, js_name = applyChanges)]
26− pub fn apply_changes(this: &ChangeList, memory: JsValue);
27−
28− #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
29− pub fn init_events_trampoline(
30− this: &ChangeList,
31− trampoline: &Closure<Fn(web_sys::Event, u32, u32)>,
32− );
33− }
34−}
35−
36−struct StringsCacheEntry {
37− key: u32,
38− used: bool,
39−}
40−
41−pub(crate) struct ChangeList {
42− bump: Bump,
43− strings_cache: FxHashMap<String, StringsCacheEntry>,
44− next_string_key: u32,
45− js: js::ChangeList,
46− events_trampoline: Option<Closure<Fn(web_sys::Event, u32, u32)>>,
47−}
48−
49−impl Drop for ChangeList {
50− fn drop(&mut self) {
51− debug!("Dropping ChangeList");
52− self.js.unmount();
53− }
54−}
55−
56−impl fmt::Debug for ChangeList {
57− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58− f.debug_struct("ChangeList")
59− .field("bump", &self.bump)
60− .field("js", &self.js)
61− .field("events_trampoline", &"..")
62− .finish()
63− }
64−}
65−
66−impl ChangeList {
67− pub(crate) fn new(container: &web_sys::Node) -> ChangeList {
68− // XXX: Because wasm-bindgen-test doesn't support third party JS
69− // dependencies, we can't use `wasm_bindgen(module = "...")` for our
70− // `ChangeList` JS import. Instead, this *should* be a local JS snippet,
71− // but that isn't implemented yet:
72− // https://github.com/rustwasm/rfcs/pull/6
73− static EVAL: Once = Once::new();
74− EVAL.call_once(|| {
75− js_sys::eval(include_str!("../js/change-list.js"))
76− .expect("should eval change-list.js OK");
77− });
78−
79− let bump = Bump::new();
80− let strings_cache = FxHashMap::default();
81− let js = js::ChangeList::new(container);
82− ChangeList {
83− bump,
84− strings_cache,
85− next_string_key: 0,
86− js,
87− events_trampoline: None,
88− }
89− }
90−
91− pub(crate) fn apply_changes(&mut self) {
92− let js = &self.js;
93− unsafe {
94− self.bump.each_allocated_chunk(|ch| {
95− js.add_change_list_range(ch.as_ptr() as usize, ch.len());
96− });
97− }
98− js.apply_changes(wasm_bindgen::memory());
99− self.bump.reset();
100− }
101−
102− pub(crate) fn init_events_trampoline(
103− &mut self,
104− trampoline: Closure<Fn(web_sys::Event, u32, u32)>,
105− ) {
106− debug_assert!(self.events_trampoline.is_none());
107− self.js.init_events_trampoline(&trampoline);
108− self.events_trampoline = Some(trampoline);
109− }
110−}
111−
112−#[repr(u32)]
113−#[derive(Clone, Copy, Debug)]
114−enum ChangeDiscriminant {
115− /// Immediates: `(pointer, length)`
116− ///
117− /// Stack: `[... TextNode] -> [... TextNode]`
118− ///
119− /// ```text
120− /// stack.top().textContent = String(pointer, length)
121− /// ```
122− SetText = 0,
123−
124− /// Immediates: `()`
125− ///
126− /// Stack: `[... Node] -> [...]`
127− ///
128− /// ```text
129− /// node = stack.pop()
130− /// while (node.nextSibling) {
131− /// node.nextSibling.remove();
132− /// }
133− /// node.remove()
134− /// ```
135− RemoveSelfAndNextSiblings = 1,
136−
137− /// Immediates: `()`
138− ///
139− /// Stack: `[... Node Node] -> [... Node]`
140− ///
141− /// ```text
142− /// new = stack.pop()
143− /// old = stack.pop()
144− /// old.replaceWith(new)
145− /// stack.push(new)
146− /// ```
147− ReplaceWith = 2,
148−
149− /// Immediates: `(pointer1, length1, pointer2, length2)`
150− ///
151− /// Stack: `[... Node] -> [... Node]`
152− ///
153− /// ```text
154− /// stack.top().setAttribute(String(pointer1, length1), String(pointer2, length2))
155− /// ```
156− SetAttribute = 3,
157−
158− /// Immediates: (pointer, length)
159− ///
160− /// Stack: `[... Node] -> [... Node]`
161− ///
162− /// ```text
163− /// stack.top().removeAttribute(String(pointer, length))
164− /// ```
165− RemoveAttribute = 4,
166−
167− /// Immediates: `()`
168− ///
169− /// Stack: `[... Node] -> [... Node node]`
170− ///
171− /// ```text
172− /// stack.push(stack.top().firstChild)
173− /// ```
174− PushFirstChild = 5,
175−
176− /// Immediates: `()`
177− ///
178− /// Stack: `[... Node] -> [... Node]`
179− ///
180− /// ```text
181− /// stack.push(stack.pop().nextSibling)
182− /// ```
183− PopPushNextSibling = 6,
184−
185− /// Immediates: `()`
186− ///
187− /// Stack: `[... T] -> [...]`
188− ///
189− /// ```text
190− /// stack.pop()
191− /// ```
192− Pop = 7,
193−
194− /// Immediates: `()`
195− ///
196− /// Stack: `[... Node Node] -> [... Node]`
197− ///
198− /// ```text
199− /// child = stack.pop()
200− /// stack.top().appendChild(child)
201− /// ```
202− AppendChild = 8,
203−
204− /// Immediates: `(pointer, length)`
205− ///
206− /// Stack: `[...] -> [... Node]`
207− ///
208− /// ```text
209− /// stack.push(createTextNode(String(pointer, length)))
210− /// ```
211− CreateTextNode = 9,
212−
213− /// Immediates: `(pointer, length)`
214− ///
215− /// Stack: `[...] -> [... Node]`
216− ///
217− /// ```text
218− /// stack.push(createElement(String(pointer, length))
219− /// ```
220− CreateElement = 10,
221−
222− /// Immediate: `(pointer, length, A, B)`
223− ///
224− /// Stack: `[... Node] -> [... Node]`
225− ///
226− /// ```text
227− /// event = String(pointer, length)
228− /// callback = ProxyToRustCallback(A, B);
229− /// stack.top().addEventListener(event, callback);
230− /// ```
231− NewEventListener = 11,
232−
233− /// Immediate: `(pointer, length, A, B)`
234− ///
235− /// Stack: `[... Node] -> [... Node]`
236− ///
237− /// ```text
238− /// event = String(pointer, length)
239− /// new_callback = ProxyToRustCallback(A, B);
240− /// stack.top().updateEventlistener(new_callback)
241− /// ```
242− UpdateEventListener = 12,
243−
244− /// Immediates: `(pointer, length)`
245− ///
246− /// Stack: `[... Node] -> [... Node]`
247− ///
248− /// ```text
249− /// stack.top().removeEventListener(String(pointer, length));
250− /// ```
251− RemoveEventListener = 13,
252−
253− /// Immediates: `(pointer, length, id)`
254− ///
255− /// Stack: `[...] -> [...]`
256− ///
257− /// ```text
258− /// addString(String(pointer, length), id);
259− /// ```
260− AddString = 14,
261−
262− /// Immediates: `(id)`
263− ///
264− /// Stack: `[...] -> [...]`
265− ///
266− /// ```text
267− /// dropString(id);
268− /// ```
269− DropString = 15,
270−}
271−
272−// Allocation utilities to ensure that we only allocation sequences of `u32`s
273−// into the change list's bump arena without any padding. This helps maintain
274−// the invariants required for `Bump::each_allocated_chunk`'s safety.
275−impl ChangeList {
276− // Allocate an opcode with zero immediates.
277− fn op0(&self, discriminant: ChangeDiscriminant) {
278− self.bump.alloc(discriminant as u32);
279− }
280−
281− // Allocate an opcode with one immediate.
282− fn op1(&self, discriminant: ChangeDiscriminant, a: u32) {
283− self.bump.alloc([discriminant as u32, a]);
284− }
285−
286− // Allocate an opcode with two immediates.
287− fn op2(&self, discriminant: ChangeDiscriminant, a: u32, b: u32) {
288− self.bump.alloc([discriminant as u32, a, b]);
289− }
290−
291− // Allocate an opcode with three immediates.
292− fn op3(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32) {
293− self.bump.alloc([discriminant as u32, a, b, c]);
294− }
295−
296− // Note: no 4-immediate opcodes at this time.
297−}
298−
299−impl ChangeList {
300− fn ensure_string(&mut self, string: &str) -> u32 {
301− if let Some(entry) = self.strings_cache.get_mut(string) {
302− entry.used = true;
303− entry.key
304− } else {
305− let key = self.next_string_key;
306− self.next_string_key += 1;
307− let entry = StringsCacheEntry { key, used: true };
308− self.strings_cache.insert(string.to_string(), entry);
309− self.op3(
310− ChangeDiscriminant::AddString,
311− string.as_ptr() as u32,
312− string.len() as u32,
313− key,
314− );
315− key
316− }
317− }
318−
319− pub(crate) fn drop_unused_strings(&mut self) {
320− debug!("drop_unused_strings()");
321− let mut new_cache = FxHashMap::default();
322− for (key, val) in self.strings_cache.iter() {
323− if val.used {
324− new_cache.insert(
325− key.clone(),
326− StringsCacheEntry {
327− key: val.key,
328− used: false,
329− },
330− );
331− } else {
332− self.op1(ChangeDiscriminant::DropString, val.key);
333− }
334− }
335− self.strings_cache = new_cache;
336− }
337−
338− pub(crate) fn emit_set_text(&self, text: &str) {
339− debug!("emit_set_text({:?})", text);
340− self.op2(
341− ChangeDiscriminant::SetText,
342− text.as_ptr() as u32,
343− text.len() as u32,
344− );
345− }
346−
347− pub(crate) fn emit_remove_self_and_next_siblings(&self) {
348− debug!("emit_remove_self_and_next_siblings()");
349− self.op0(ChangeDiscriminant::RemoveSelfAndNextSiblings);
350− }
351−
352− pub(crate) fn emit_replace_with(&self) {
353− debug!("emit_replace_with()");
354− self.op0(ChangeDiscriminant::ReplaceWith);
355− }
356−
357− pub(crate) fn emit_set_attribute(&mut self, name: &str, value: &str) {
358− debug!("emit_set_attribute({:?}, {:?})", name, value);
359− let name_id = self.ensure_string(name);
360− let value_id = self.ensure_string(value);
361− self.op2(ChangeDiscriminant::SetAttribute, name_id, value_id);
362− }
363−
364− pub(crate) fn emit_remove_attribute(&mut self, name: &str) {
365− debug!("emit_remove_attribute({:?})", name);
366− let name_id = self.ensure_string(name);
367− self.op1(ChangeDiscriminant::RemoveAttribute, name_id);
368− }
369−
370− pub(crate) fn emit_push_first_child(&self) {
371− debug!("emit_push_first_child()");
372− self.op0(ChangeDiscriminant::PushFirstChild);
373− }
374−
375− pub(crate) fn emit_pop_push_next_sibling(&self) {
376− debug!("emit_pop_push_next_sibling()");
377− self.op0(ChangeDiscriminant::PopPushNextSibling);
378− }
379−
380− pub(crate) fn emit_pop(&self) {
381− debug!("emit_pop()");
382− self.op0(ChangeDiscriminant::Pop);
383− }
384−
385− pub(crate) fn emit_append_child(&self) {
386− debug!("emit_append_child()");
387− self.op0(ChangeDiscriminant::AppendChild);
388− }
389−
390− pub(crate) fn emit_create_text_node(&self, text: &str) {
391− debug!("emit_create_text_node({:?})", text);
392− self.op2(
393− ChangeDiscriminant::CreateTextNode,
394− text.as_ptr() as u32,
395− text.len() as u32,
396− );
397− }
398−
399− pub(crate) fn emit_create_element(&mut self, tag_name: &str) {
400− debug!("emit_create_element({:?})", tag_name);
401− let tag_name_id = self.ensure_string(tag_name);
402− self.op1(ChangeDiscriminant::CreateElement, tag_name_id);
403− }
404−
405− pub(crate) fn emit_new_event_listener(&mut self, listener: &Listener) {
406− debug!("emit_new_event_listener({:?})", listener);
407− let (a, b) = listener.get_callback_parts();
408− debug_assert!(a != 0);
409− let event_id = self.ensure_string(listener.event);
410− self.op3(ChangeDiscriminant::NewEventListener, event_id, a, b);
411− }
412−
413− pub(crate) fn emit_update_event_listener(&mut self, listener: &Listener) {
414− debug!("emit_update_event_listener({:?})", listener);
415− let (a, b) = listener.get_callback_parts();
416− debug_assert!(a != 0);
417− let event_id = self.ensure_string(listener.event);
418− self.op3(ChangeDiscriminant::UpdateEventListener, event_id, a, b);
419− }
420−
421− pub(crate) fn emit_remove_event_listener(&mut self, event: &str) {
422− debug!("emit_remove_event_listener({:?})", event);
423− let event_id = self.ensure_string(event);
424− self.op1(ChangeDiscriminant::RemoveEventListener, event_id);
425− }
426−}
src/change_list/emitter.rsnew file+292
@@ -0,0 +1,292 @@
1+//! The `InstructionEmitter` is responsible for encoding change list
2+//! instructions and ensuring that each instruction has the correct number of
3+//! immediates. It ensures that the resulting change list instruction stream is
4+//! *syntactically* correct (opcodes have the correct arity of immediate
5+//! arguments, etc), while `ChangeListBuilder` wraps an emitter and additionally
6+//! ensures that the resulting change list program is *semantically* correct
7+//! (doesn't reference cached strings before they've been added to the cache,
8+//! etc).
9+//!
10+//! We encode the instructions directly into a dedicated bump arena. We
11+//! eventually pass the bump arena's chunks to the interpreter in JS, so it is
12+//! critical that nothing other than change list instructions are allocated
13+//! inside this bump, and that the instructions themselves do not contain any
14+//! padding or uninitialized memory. See the documentation for the the
15+//! `Bump::each_allocated_chunk` method for details.
16+
17+use bumpalo::Bump;
18+
19+#[derive(Debug)]
20+pub(crate) struct InstructionEmitter {
21+ bump: Bump,
22+}
23+
24+impl InstructionEmitter {
25+ /// Construct a new `InstructionEmitter` with its own bump arena.
26+ pub fn new() -> InstructionEmitter {
27+ let bump = Bump::new();
28+ InstructionEmitter { bump }
29+ }
30+
31+ /// Invoke the given function with each of the allocated instruction
32+ /// sequences that this emitter has built up.
33+ #[cfg_attr(feature = "xxx-unstable-internal-use-only", allow(dead_code))]
34+ pub fn each_instruction_sequence<F>(&mut self, f: F)
35+ where
36+ F: FnMut(&[u8]),
37+ {
38+ // Note: the safety invariants required for `each_allocated_chunk` are
39+ // maintained by the fact that everything we encode as allocations in
40+ // the bump arena are in the form of `u32`s, and therefore we don't have
41+ // any uninitialized memory padding in the arena.
42+ unsafe {
43+ self.bump.each_allocated_chunk(f);
44+ }
45+ }
46+
47+ /// Reset to an empty sequence of instructions.
48+ pub fn reset(&mut self) {
49+ self.bump.reset();
50+ }
51+}
52+
53+macro_rules! define_change_list_instructions {
54+ ( $(
55+ $( #[$attr:meta] )*
56+ $name:ident (
57+ $($immediate:ident),*
58+ ) = $discriminant:expr,
59+ )* ) => {
60+ impl InstructionEmitter {
61+ $(
62+ $( #[$attr] )*
63+ #[inline]
64+ pub fn $name(&self $(, $immediate: u32)*) {
65+ self.bump.alloc_with(|| [$discriminant $(, $immediate )* ]);
66+ }
67+ )*
68+ }
69+ }
70+}
71+
72+define_change_list_instructions! {
73+ /// Stack: `[... TextNode] -> [... TextNode]`
74+ ///
75+ /// ```text
76+ /// stack.top().textContent = readString(pointer, length)
77+ /// ```
78+ set_text(pointer, length) = 0,
79+
80+ /// Stack: `[... Node] -> [...]`
81+ ///
82+ /// ```text
83+ /// node = stack.pop()
84+ /// while (node.nextSibling) {
85+ /// node.nextSibling.remove();
86+ /// }
87+ /// node.remove()
88+ /// ```
89+ remove_self_and_next_siblings() = 1,
90+
91+ /// Stack: `[... Node Node] -> [... Node]`
92+ ///
93+ /// ```text
94+ /// new = stack.pop()
95+ /// old = stack.pop()
96+ /// old.replaceWith(new)
97+ /// stack.push(new)
98+ /// ```
99+ replace_with() = 2,
100+
101+ /// Stack: `[... Node] -> [... Node]`
102+ ///
103+ /// ```text
104+ /// stack.top().setAttribute(getCachedString(attribute_key), getCachedString(value_key))
105+ /// ```
106+ set_attribute(attribute_key, value_key) = 3,
107+
108+ /// Stack: `[... Node] -> [... Node]`
109+ ///
110+ /// ```text
111+ /// stack.top().removeAttribute(getCachedString(attribute_key))
112+ /// ```
113+ remove_attribute(attribute_key) = 4,
114+
115+ /// Stack: `[... Node] -> [... Node Node]`
116+ ///
117+ /// ```text
118+ /// parent = stack.top()
119+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
120+ /// stack.push(child)
121+ /// ```
122+ push_reverse_child(n) = 5,
123+
124+ /// Stack: `[... Node Node] -> [... Node Node]`
125+ ///
126+ /// ```text
127+ /// stack.pop();
128+ /// parent = stack.top();
129+ /// child = parent.childNodes[n]
130+ /// stack.push(child)
131+ /// ```
132+ pop_push_child(n) = 6,
133+
134+ /// Stack: `[... T] -> [...]`
135+ ///
136+ /// ```text
137+ /// stack.pop()
138+ /// ```
139+ pop() = 7,
140+
141+ /// Stack: `[... Node Node] -> [... Node]`
142+ ///
143+ /// ```text
144+ /// child = stack.pop()
145+ /// stack.top().appendChild(child)
146+ /// ```
147+ append_child() = 8,
148+
149+ /// Stack: `[...] -> [... Node]`
150+ ///
151+ /// ```text
152+ /// stack.push(document.createTextNode(readString(pointer, length)))
153+ /// ```
154+ create_text_node(pointer, length) = 9,
155+
156+ /// Stack: `[...] -> [... Node]`
157+ ///
158+ /// ```text
159+ /// stack.push(document.createElement(getCachedString(tag_name_key))
160+ /// ```
161+ create_element(tag_name_key) = 10,
162+
163+ /// Stack: `[... Node] -> [... Node]`
164+ ///
165+ /// ```text
166+ /// event = getCachedString(event_key)
167+ /// callback = createProxyToRustCallback(a, b)
168+ /// stack.top().addEventListener(event, callback)
169+ /// ```
170+ new_event_listener(event_key, a, b) = 11,
171+
172+ /// Stack: `[... Node] -> [... Node]`
173+ ///
174+ /// ```text
175+ /// event = getCachedString(event_key)
176+ /// new_callback = createProxyToRustCallback(a, b);
177+ /// stack.top().updateEventlistener(new_callback)
178+ /// ```
179+ update_event_listener(event_key, a, b) = 12,
180+
181+ /// Stack: `[... Node] -> [... Node]`
182+ ///
183+ /// ```text
184+ /// stack.top().removeEventListener(getCachedString(event_key));
185+ /// ```
186+ remove_event_listener(event_key) = 13,
187+
188+ /// Stack: `[...] -> [...]`
189+ ///
190+ /// ```text
191+ /// addCachedString(readString(pointer, length), key);
192+ /// ```
193+ add_cached_string(pointer, length, key) = 14,
194+
195+ /// Stack: `[...] -> [...]`
196+ ///
197+ /// ```text
198+ /// dropCachedString(key);
199+ /// ```
200+ drop_cached_string(key) = 15,
201+
202+ /// Stack: `[...] -> [... Node]`
203+ ///
204+ /// ```text
205+ /// tag_name = getCachedString(tag_name_key)
206+ /// namespace = getCachedString(tag_name_key)
207+ /// stack.push(document.createElementNS(tag_name, namespace))
208+ /// ```
209+ create_element_ns(tag_name_key, namespace_key) = 16,
210+
211+ /// Stack: `[...] -> [...]`
212+ ///
213+ /// ```text
214+ /// parent = stack.top()
215+ /// children = parent.childNodes
216+ /// temp = temp_base
217+ /// for i in start .. end:
218+ /// temporaries[temp] = children[i]
219+ /// temp += 1
220+ /// ```
221+ save_children_to_temporaries(temp_base, start, end) = 17,
222+
223+ /// Stack: `[... Node] -> [... Node Node]`
224+ ///
225+ /// ```text
226+ /// parent = stack.top()
227+ /// child = parent.childNodes[n]
228+ /// stack.push(child)
229+ /// ```
230+ push_child(n) = 18,
231+
232+ /// Stack: `[...] -> [... Node]`
233+ ///
234+ /// ```text
235+ /// stack.push(temporaries[temp])
236+ /// ```
237+ push_temporary(temp) = 19,
238+
239+ /// Stack: `[... Node Node] -> [... Node]`
240+ ///
241+ /// ```text
242+ /// before = stack.pop()
243+ /// after = stack.pop()
244+ /// after.insertBefore(before)
245+ /// stack.push(before)
246+ /// ```
247+ insert_before() = 20,
248+
249+ /// Stack: `[... Node Node] -> [... Node Node]`
250+ ///
251+ /// ```text
252+ /// stack.pop()
253+ /// parent = stack.top()
254+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
255+ /// stack.push(child)
256+ /// ```
257+ pop_push_reverse_child(n) = 21,
258+
259+ /// Stack: `[... Node] -> [... Node]`
260+ ///
261+ /// ```text
262+ /// parent = stack.top()
263+ /// child = parent.childNodes[n]
264+ /// child.remove()
265+ /// ```
266+ remove_child(n) = 22,
267+
268+ /// Stack: `[... Node] -> [... Node]`
269+ ///
270+ /// ```text
271+ /// class = getCachedString(class)
272+ /// node = stack.top()
273+ /// node.className = class
274+ /// ```
275+ set_class(class) = 23,
276+
277+ /// Stack: `[... Node] -> [... Node]`
278+ ///
279+ /// ```text
280+ /// template = stack.top()
281+ /// saveTemplate(id, template)
282+ /// ```
283+ save_template(id) = 24,
284+
285+ /// Stack: `[...] -> [... Node]`
286+ ///
287+ /// ```text
288+ /// template = getTemplate(id)
289+ /// stack.push(template.cloneNode(true))
290+ /// ```
291+ push_template(id) = 25,
292+}
src/change_list/js.rsnew file+40
@@ -0,0 +1,40 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
3+ #[derive(Clone, Debug)]
4+ pub struct ChangeListInterpreter {}
5+ impl ChangeListInterpreter {
6+ pub fn new(_container: &crate::Element) -> ChangeListInterpreter {
7+ ChangeListInterpreter {}
8+ }
9+ pub fn unmount(&self) {}
10+ pub fn add_change_list_range(&self, _start: usize, _len: usize) {}
11+ pub fn init_events_trampoline(&self, _trampoline: &crate::EventsTrampoline) {}
12+ }
13+ } else {
14+ use wasm_bindgen::prelude::*;
15+
16+ #[wasm_bindgen(module = "/js/change-list-interpreter.js")]
17+ extern "C" {
18+ #[derive(Clone, Debug)]
19+ pub type ChangeListInterpreter;
20+
21+ #[wasm_bindgen(constructor)]
22+ pub fn new(container: &web_sys::Element) -> ChangeListInterpreter;
23+
24+ #[wasm_bindgen(structural, method)]
25+ pub fn unmount(this: &ChangeListInterpreter);
26+
27+ #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
28+ pub fn add_change_list_range(this: &ChangeListInterpreter, start: usize, len: usize);
29+
30+ #[wasm_bindgen(structural, method, js_name = applyChanges)]
31+ pub fn apply_changes(this: &ChangeListInterpreter, memory: JsValue);
32+
33+ #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
34+ pub fn init_events_trampoline(
35+ this: &ChangeListInterpreter,
36+ trampoline: &crate::EventsTrampoline,
37+ );
38+ }
39+ }
40+}
src/change_list/mod.rsnew file+353
@@ -0,0 +1,353 @@
1+pub(crate) mod emitter;
2+pub(crate) mod strings;
3+pub(crate) mod traversal;
4+
5+// Note: has to be `pub` because of `wasm-bindgen` visibility restrictions.
6+pub mod js;
7+
8+use self::emitter::InstructionEmitter;
9+use self::strings::{StringKey, StringsCache};
10+use self::traversal::{MoveTo, Traversal};
11+use crate::{cached_set::CacheId, Listener};
12+use fxhash::FxHashSet;
13+
14+#[derive(Debug)]
15+pub(crate) struct ChangeListPersistentState {
16+ strings: StringsCache,
17+ emitter: InstructionEmitter,
18+ traversal: Traversal,
19+ interpreter: js::ChangeListInterpreter,
20+ templates: FxHashSet<CacheId>,
21+}
22+
23+pub(crate) struct ChangeListBuilder<'a> {
24+ state: &'a mut ChangeListPersistentState,
25+ next_temporary: u32,
26+ forcing_new_listeners: bool,
27+}
28+
29+impl Drop for ChangeListPersistentState {
30+ fn drop(&mut self) {
31+ self.interpreter.unmount();
32+ }
33+}
34+
35+impl ChangeListPersistentState {
36+ pub(crate) fn new(container: &crate::Element) -> ChangeListPersistentState {
37+ let strings = StringsCache::new();
38+ let emitter = InstructionEmitter::new();
39+ let traversal = Traversal::new();
40+ let interpreter = js::ChangeListInterpreter::new(container);
41+ let templates = Default::default();
42+ ChangeListPersistentState {
43+ strings,
44+ emitter,
45+ traversal,
46+ interpreter,
47+ templates,
48+ }
49+ }
50+
51+ pub(crate) fn init_events_trampoline(&mut self, trampoline: &crate::EventsTrampoline) {
52+ self.interpreter.init_events_trampoline(trampoline);
53+ }
54+
55+ pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
56+ ChangeListBuilder {
57+ state: self,
58+ next_temporary: 0,
59+ forcing_new_listeners: false,
60+ }
61+ }
62+}
63+
64+cfg_if::cfg_if! {
65+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
66+ impl ChangeListBuilder<'_> {
67+ pub(crate) fn finish(self) {
68+ self.state.strings.drop_unused_strings(&self.state.emitter);
69+
70+ // Nothing to actually apply the changes to.
71+
72+ self.state.emitter.reset();
73+ self.state.traversal.reset();
74+ }
75+ }
76+ } else {
77+ impl ChangeListBuilder<'_> {
78+ pub(crate) fn finish(self) {
79+ self.state.strings.drop_unused_strings(&self.state.emitter);
80+
81+ // Apply the changes.
82+ let interpreter = &self.state.interpreter;
83+ self.state.emitter.each_instruction_sequence(|seq| {
84+ interpreter.add_change_list_range(seq.as_ptr() as usize, seq.len());
85+ });
86+ interpreter.apply_changes(wasm_bindgen::memory());
87+
88+ self.state.emitter.reset();
89+ self.state.traversal.reset();
90+ }
91+ }
92+ }
93+}
94+
95+/// Traversal methods.
96+impl ChangeListBuilder<'_> {
97+ pub fn go_down(&mut self) {
98+ self.state.traversal.down();
99+ }
100+
101+ pub fn go_down_to_child(&mut self, index: usize) {
102+ self.state.traversal.down();
103+ self.state.traversal.sibling(index);
104+ }
105+
106+ pub fn go_down_to_reverse_child(&mut self, index: usize) {
107+ self.state.traversal.down();
108+ self.state.traversal.reverse_sibling(index);
109+ }
110+
111+ pub fn go_up(&mut self) {
112+ self.state.traversal.up();
113+ }
114+
115+ pub fn go_to_sibling(&mut self, index: usize) {
116+ self.state.traversal.sibling(index);
117+ }
118+
119+ pub fn go_to_temp_sibling(&mut self, temp: u32) {
120+ self.state.traversal.up();
121+ self.state.traversal.down_to_temp(temp);
122+ }
123+
124+ pub fn go_down_to_temp_child(&mut self, temp: u32) {
125+ self.state.traversal.down_to_temp(temp);
126+ }
127+
128+ pub fn commit_traversal(&mut self) {
129+ if self.state.traversal.is_committed() {
130+ return;
131+ }
132+
133+ for mv in self.state.traversal.commit() {
134+ match mv {
135+ MoveTo::Parent => {
136+ debug!("emit: pop");
137+ self.state.emitter.pop();
138+ }
139+ MoveTo::Child(n) => {
140+ debug!("emit: push_child({})", n);
141+ self.state.emitter.push_child(n);
142+ }
143+ MoveTo::ReverseChild(n) => {
144+ debug!("emit: push_reverse_child({})", n);
145+ self.state.emitter.push_reverse_child(n);
146+ }
147+ MoveTo::Sibling(n) => {
148+ debug!("emit: pop_push_child({})", n);
149+ self.state.emitter.pop_push_child(n);
150+ }
151+ MoveTo::ReverseSibling(n) => {
152+ debug!("emit: pop_push_reverse_child({})", n);
153+ self.state.emitter.pop_push_reverse_child(n);
154+ }
155+ MoveTo::TempChild(temp) => {
156+ debug!("emit: push_temporary({})", temp);
157+ self.state.emitter.push_temporary(temp);
158+ }
159+ }
160+ }
161+ }
162+
163+ pub fn traversal_is_committed(&self) -> bool {
164+ self.state.traversal.is_committed()
165+ }
166+}
167+
168+impl ChangeListBuilder<'_> {
169+ pub fn next_temporary(&self) -> u32 {
170+ self.next_temporary
171+ }
172+
173+ pub fn set_next_temporary(&mut self, next_temporary: u32) {
174+ self.next_temporary = next_temporary;
175+ }
176+
177+ pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
178+ debug_assert!(self.traversal_is_committed());
179+ debug_assert!(start < end);
180+ let temp_base = self.next_temporary;
181+ debug!(
182+ "emit: save_children_to_temporaries({}, {}, {})",
183+ temp_base, start, end
184+ );
185+ self.next_temporary = temp_base + (end - start) as u32;
186+ self.state
187+ .emitter
188+ .save_children_to_temporaries(temp_base, start as u32, end as u32);
189+ temp_base
190+ }
191+
192+ pub fn push_temporary(&self, temp: u32) {
193+ debug_assert!(self.traversal_is_committed());
194+ debug!("emit: push_temporary({})", temp);
195+ self.state.emitter.push_temporary(temp);
196+ }
197+
198+ pub fn remove_child(&self, child: usize) {
199+ debug_assert!(self.traversal_is_committed());
200+ debug!("emit: remove_child({})", child);
201+ self.state.emitter.remove_child(child as u32);
202+ }
203+
204+ pub fn insert_before(&self) {
205+ debug_assert!(self.traversal_is_committed());
206+ debug!("emit: insert_before()");
207+ self.state.emitter.insert_before();
208+ }
209+
210+ pub fn ensure_string(&mut self, string: &str) -> StringKey {
211+ self.state
212+ .strings
213+ .ensure_string(string, &self.state.emitter)
214+ }
215+
216+ pub fn set_text(&self, text: &str) {
217+ debug_assert!(self.traversal_is_committed());
218+ debug!("emit: set_text({:?})", text);
219+ self.state
220+ .emitter
221+ .set_text(text.as_ptr() as u32, text.len() as u32);
222+ }
223+
224+ pub fn remove_self_and_next_siblings(&self) {
225+ debug_assert!(self.traversal_is_committed());
226+ debug!("emit: remove_self_and_next_siblings()");
227+ self.state.emitter.remove_self_and_next_siblings();
228+ }
229+
230+ pub fn replace_with(&self) {
231+ debug_assert!(self.traversal_is_committed());
232+ debug!("emit: replace_with()");
233+ self.state.emitter.replace_with();
234+ }
235+
236+ pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
237+ debug_assert!(self.traversal_is_committed());
238+ if name == "class" && !is_namespaced {
239+ let class_id = self.ensure_string(value);
240+ debug!("emit: set_class({:?})", value);
241+ self.state.emitter.set_class(class_id.into());
242+ } else {
243+ let name_id = self.ensure_string(name);
244+ let value_id = self.ensure_string(value);
245+ debug!("emit: set_attribute({:?}, {:?})", name, value);
246+ self.state
247+ .emitter
248+ .set_attribute(name_id.into(), value_id.into());
249+ }
250+ }
251+
252+ pub fn remove_attribute(&mut self, name: &str) {
253+ debug_assert!(self.traversal_is_committed());
254+ debug!("emit: remove_attribute({:?})", name);
255+ let name_id = self.ensure_string(name);
256+ self.state.emitter.remove_attribute(name_id.into());
257+ }
258+
259+ pub fn append_child(&self) {
260+ debug_assert!(self.traversal_is_committed());
261+ debug!("emit: append_child()");
262+ self.state.emitter.append_child();
263+ }
264+
265+ pub fn create_text_node(&self, text: &str) {
266+ debug_assert!(self.traversal_is_committed());
267+ debug!("emit: create_text_node({:?})", text);
268+ self.state
269+ .emitter
270+ .create_text_node(text.as_ptr() as u32, text.len() as u32);
271+ }
272+
273+ pub fn create_element(&mut self, tag_name: &str) {
274+ debug_assert!(self.traversal_is_committed());
275+ debug!("emit: create_element({:?})", tag_name);
276+ let tag_name_id = self.ensure_string(tag_name);
277+ self.state.emitter.create_element(tag_name_id.into());
278+ }
279+
280+ pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) {
281+ debug_assert!(self.traversal_is_committed());
282+ debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns);
283+ let tag_name_id = self.ensure_string(tag_name);
284+ let ns_id = self.ensure_string(ns);
285+ self.state
286+ .emitter
287+ .create_element_ns(tag_name_id.into(), ns_id.into());
288+ }
289+
290+ pub fn push_force_new_listeners(&mut self) -> bool {
291+ let old = self.forcing_new_listeners;
292+ self.forcing_new_listeners = true;
293+ old
294+ }
295+
296+ pub fn pop_force_new_listeners(&mut self, previous: bool) {
297+ debug_assert!(self.forcing_new_listeners);
298+ self.forcing_new_listeners = previous;
299+ }
300+
301+ pub fn new_event_listener(&mut self, listener: &Listener) {
302+ debug_assert!(self.traversal_is_committed());
303+ debug!("emit: new_event_listener({:?})", listener);
304+ let (a, b) = listener.get_callback_parts();
305+ debug_assert!(a != 0);
306+ let event_id = self.ensure_string(listener.event);
307+ self.state.emitter.new_event_listener(event_id.into(), a, b);
308+ }
309+
310+ pub fn update_event_listener(&mut self, listener: &Listener) {
311+ debug_assert!(self.traversal_is_committed());
312+
313+ if self.forcing_new_listeners {
314+ self.new_event_listener(listener);
315+ return;
316+ }
317+
318+ debug!("emit: update_event_listener({:?})", listener);
319+ let (a, b) = listener.get_callback_parts();
320+ debug_assert!(a != 0);
321+ let event_id = self.ensure_string(listener.event);
322+ self.state
323+ .emitter
324+ .update_event_listener(event_id.into(), a, b);
325+ }
326+
327+ pub fn remove_event_listener(&mut self, event: &str) {
328+ debug_assert!(self.traversal_is_committed());
329+ debug!("emit: remove_event_listener({:?})", event);
330+ let event_id = self.ensure_string(event);
331+ self.state.emitter.remove_event_listener(event_id.into());
332+ }
333+
334+ #[inline]
335+ pub fn has_template(&mut self, id: CacheId) -> bool {
336+ self.state.templates.contains(&id)
337+ }
338+
339+ pub fn save_template(&mut self, id: CacheId) {
340+ debug_assert!(self.traversal_is_committed());
341+ debug_assert!(!self.has_template(id));
342+ debug!("emit: save_template({:?})", id);
343+ self.state.templates.insert(id);
344+ self.state.emitter.save_template(id.into());
345+ }
346+
347+ pub fn push_template(&mut self, id: CacheId) {
348+ debug_assert!(self.traversal_is_committed());
349+ debug_assert!(self.has_template(id));
350+ debug!("emit: push_template({:?})", id);
351+ self.state.emitter.push_template(id.into());
352+ }
353+}
src/change_list/strings.rsnew file+66
@@ -0,0 +1,66 @@
1+use crate::change_list::emitter::InstructionEmitter;
2+use fxhash::FxHashMap;
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub struct StringKey(u32);
6+
7+impl From<StringKey> for u32 {
8+ #[inline]
9+ fn from(key: StringKey) -> u32 {
10+ key.0
11+ }
12+}
13+
14+#[derive(Debug)]
15+struct StringsCacheEntry {
16+ key: StringKey,
17+ used: bool,
18+}
19+
20+#[derive(Debug, Default)]
21+pub(crate) struct StringsCache {
22+ entries: FxHashMap<String, StringsCacheEntry>,
23+ next_string_key: u32,
24+}
25+
26+impl StringsCache {
27+ /// Create a new, empty strings cache.
28+ pub fn new() -> StringsCache {
29+ Default::default()
30+ }
31+
32+ /// Ensure that the given string is cached, and get its key.
33+ pub fn ensure_string(&mut self, string: &str, emitter: &InstructionEmitter) -> StringKey {
34+ if let Some(entry) = self.entries.get_mut(string) {
35+ entry.used = true;
36+ entry.key
37+ } else {
38+ let key = StringKey(self.next_string_key);
39+ self.next_string_key += 1;
40+ let entry = StringsCacheEntry { key, used: true };
41+ self.entries.insert(string.to_string(), entry);
42+ emitter.add_cached_string(string.as_ptr() as u32, string.len() as u32, key.into());
43+ key
44+ }
45+ }
46+
47+ pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) {
48+ self.entries.retain(|string, entry| {
49+ if entry.used {
50+ // Since this entry was used during while rendering this frame,
51+ // it is likely going to be used for the next frame as well. So
52+ // we keep it in the cache so that we don't have to repopulate
53+ // and resync the cache next frame (assuming it is reused), but
54+ // we set the `used` flag to false so that if it is not used
55+ // next frame, it will be cleaned up.
56+ entry.used = false;
57+ true
58+ } else {
59+ let key = entry.key.into();
60+ debug!("emit: drop_cached_string({}) = {:?}", key, string);
61+ emitter.drop_cached_string(key);
62+ false
63+ }
64+ });
65+ }
66+}
src/change_list/traversal.rsnew file+344
@@ -0,0 +1,344 @@
1+//! Keeps track of where we are moving in a DOM tree, and shortens traversal
2+//! paths between mutations to their minimal number of operations.
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub enum MoveTo {
6+ /// Move from the current node up to its parent.
7+ Parent,
8+
9+ /// Move to the current node's n^th child.
10+ Child(u32),
11+
12+ /// Move to the current node's n^th from last child.
13+ ReverseChild(u32),
14+
15+ /// Move to the n^th sibling. Not relative from the current
16+ /// location. Absolute indexed within all of the current siblings.
17+ Sibling(u32),
18+
19+ /// Move to the n^th from last sibling. Not relative from the current
20+ /// location. Absolute indexed within all of the current siblings.
21+ ReverseSibling(u32),
22+
23+ /// Move down to the given saved temporary child.
24+ TempChild(u32),
25+}
26+
27+#[derive(Debug)]
28+pub struct Traversal {
29+ uncommitted: Vec<MoveTo>,
30+}
31+
32+impl Traversal {
33+ /// Construct a new `Traversal` with its internal storage backed by the
34+ /// given bump arena.
35+ pub fn new() -> Traversal {
36+ Traversal {
37+ uncommitted: Vec::with_capacity(32),
38+ }
39+ }
40+
41+ /// Move the traversal up in the tree.
42+ pub fn up(&mut self) {
43+ match self.uncommitted.last() {
44+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
45+ self.uncommitted.pop();
46+ self.uncommitted.push(MoveTo::Parent);
47+ }
48+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => {
49+ self.uncommitted.pop();
50+ // And we're back at the parent.
51+ }
52+ _ => {
53+ self.uncommitted.push(MoveTo::Parent);
54+ }
55+ }
56+ }
57+
58+ /// Move the traversal down in the tree to the first child of the current
59+ /// node.
60+ pub fn down(&mut self) {
61+ if let Some(&MoveTo::Parent) = self.uncommitted.last() {
62+ self.uncommitted.pop();
63+ self.sibling(0);
64+ } else {
65+ self.uncommitted.push(MoveTo::Child(0));
66+ }
67+ }
68+
69+ /// Move the traversal to the n^th sibling.
70+ pub fn sibling(&mut self, index: usize) {
71+ let index = index as u32;
72+ match self.uncommitted.last_mut() {
73+ Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => {
74+ *n = index;
75+ }
76+ Some(MoveTo::ReverseSibling(_)) => {
77+ self.uncommitted.pop();
78+ self.uncommitted.push(MoveTo::Sibling(index));
79+ }
80+ Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => {
81+ self.uncommitted.pop();
82+ self.uncommitted.push(MoveTo::Child(index))
83+ }
84+ _ => {
85+ self.uncommitted.push(MoveTo::Sibling(index));
86+ }
87+ }
88+ }
89+
90+ /// Move the the n^th from last sibling.
91+ pub fn reverse_sibling(&mut self, index: usize) {
92+ let index = index as u32;
93+ match self.uncommitted.last_mut() {
94+ Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => {
95+ *n = index;
96+ }
97+ Some(MoveTo::Sibling(_)) => {
98+ self.uncommitted.pop();
99+ self.uncommitted.push(MoveTo::ReverseSibling(index));
100+ }
101+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => {
102+ self.uncommitted.pop();
103+ self.uncommitted.push(MoveTo::ReverseChild(index))
104+ }
105+ _ => {
106+ self.uncommitted.push(MoveTo::ReverseSibling(index));
107+ }
108+ }
109+ }
110+
111+ /// Go to the given saved temporary.
112+ pub fn down_to_temp(&mut self, temp: u32) {
113+ match self.uncommitted.last() {
114+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
115+ self.uncommitted.pop();
116+ }
117+ Some(MoveTo::Parent)
118+ | Some(MoveTo::TempChild(_))
119+ | Some(MoveTo::Child(_))
120+ | Some(MoveTo::ReverseChild(_))
121+ | None => {
122+ // Can't remove moves to parents since we rely on their stack
123+ // pops.
124+ }
125+ }
126+ self.uncommitted.push(MoveTo::TempChild(temp));
127+ }
128+
129+ /// Are all the traversal's moves committed? That is, are there no moves
130+ /// that have *not* been committed yet?
131+ #[inline]
132+ pub fn is_committed(&self) -> bool {
133+ self.uncommitted.is_empty()
134+ }
135+
136+ /// Commit this traversals moves and return the optimized path from the last
137+ /// commit.
138+ #[inline]
139+ pub fn commit(&mut self) -> Moves {
140+ Moves {
141+ inner: self.uncommitted.drain(..),
142+ }
143+ }
144+
145+ #[inline]
146+ pub fn reset(&mut self) {
147+ self.uncommitted.clear();
148+ }
149+}
150+
151+pub struct Moves<'a> {
152+ inner: std::vec::Drain<'a, MoveTo>,
153+}
154+
155+impl Iterator for Moves<'_> {
156+ type Item = MoveTo;
157+
158+ #[inline]
159+ fn next(&mut self) -> Option<MoveTo> {
160+ self.inner.next()
161+ }
162+}
163+
164+#[cfg(test)]
165+mod tests {
166+ use super::*;
167+
168+ #[test]
169+ fn test_traversal() {
170+ fn t<F>(f: F) -> Box<dyn FnMut(&mut Traversal)>
171+ where
172+ F: 'static + FnMut(&mut Traversal),
173+ {
174+ Box::new(f) as _
175+ }
176+
177+ for (mut traverse, expected_moves) in vec![
178+ (
179+ t(|t| {
180+ t.down();
181+ }),
182+ vec![MoveTo::Child(0)],
183+ ),
184+ (
185+ t(|t| {
186+ t.up();
187+ }),
188+ vec![MoveTo::Parent],
189+ ),
190+ (
191+ t(|t| {
192+ t.sibling(42);
193+ }),
194+ vec![MoveTo::Sibling(42)],
195+ ),
196+ (
197+ t(|t| {
198+ t.down();
199+ t.up();
200+ }),
201+ vec![],
202+ ),
203+ (
204+ t(|t| {
205+ t.down();
206+ t.sibling(2);
207+ t.up();
208+ }),
209+ vec![],
210+ ),
211+ (
212+ t(|t| {
213+ t.down();
214+ t.sibling(3);
215+ }),
216+ vec![MoveTo::Child(3)],
217+ ),
218+ (
219+ t(|t| {
220+ t.down();
221+ t.sibling(4);
222+ t.sibling(8);
223+ }),
224+ vec![MoveTo::Child(8)],
225+ ),
226+ (
227+ t(|t| {
228+ t.sibling(1);
229+ t.sibling(1);
230+ }),
231+ vec![MoveTo::Sibling(1)],
232+ ),
233+ (
234+ t(|t| {
235+ t.reverse_sibling(3);
236+ }),
237+ vec![MoveTo::ReverseSibling(3)],
238+ ),
239+ (
240+ t(|t| {
241+ t.down();
242+ t.reverse_sibling(3);
243+ }),
244+ vec![MoveTo::ReverseChild(3)],
245+ ),
246+ (
247+ t(|t| {
248+ t.down();
249+ t.reverse_sibling(3);
250+ t.up();
251+ }),
252+ vec![],
253+ ),
254+ (
255+ t(|t| {
256+ t.down();
257+ t.reverse_sibling(3);
258+ t.reverse_sibling(6);
259+ }),
260+ vec![MoveTo::ReverseChild(6)],
261+ ),
262+ (
263+ t(|t| {
264+ t.up();
265+ t.reverse_sibling(3);
266+ t.reverse_sibling(6);
267+ }),
268+ vec![MoveTo::Parent, MoveTo::ReverseSibling(6)],
269+ ),
270+ (
271+ t(|t| {
272+ t.up();
273+ t.sibling(3);
274+ t.sibling(6);
275+ }),
276+ vec![MoveTo::Parent, MoveTo::Sibling(6)],
277+ ),
278+ (
279+ t(|t| {
280+ t.sibling(3);
281+ t.sibling(6);
282+ t.up();
283+ }),
284+ vec![MoveTo::Parent],
285+ ),
286+ (
287+ t(|t| {
288+ t.reverse_sibling(3);
289+ t.reverse_sibling(6);
290+ t.up();
291+ }),
292+ vec![MoveTo::Parent],
293+ ),
294+ (
295+ t(|t| {
296+ t.down();
297+ t.down_to_temp(3);
298+ }),
299+ vec![MoveTo::Child(0), MoveTo::TempChild(3)],
300+ ),
301+ (
302+ t(|t| {
303+ t.down_to_temp(3);
304+ t.sibling(5);
305+ }),
306+ vec![MoveTo::Child(5)],
307+ ),
308+ (
309+ t(|t| {
310+ t.down_to_temp(3);
311+ t.reverse_sibling(5);
312+ }),
313+ vec![MoveTo::ReverseChild(5)],
314+ ),
315+ (
316+ t(|t| {
317+ t.down_to_temp(3);
318+ t.up();
319+ }),
320+ vec![],
321+ ),
322+ (
323+ t(|t| {
324+ t.sibling(2);
325+ t.up();
326+ t.down_to_temp(3);
327+ }),
328+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
329+ ),
330+ (
331+ t(|t| {
332+ t.up();
333+ t.down_to_temp(3);
334+ }),
335+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
336+ ),
337+ ] {
338+ let mut traversal = Traversal::new();
339+ traverse(&mut traversal);
340+ let actual_moves: Vec<_> = traversal.commit().collect();
341+ assert_eq!(actual_moves, expected_moves);
342+ }
343+ }
344+}
src/diff.rsnew file+1125
@@ -0,0 +1,1125 @@
1+use crate::{
2+ cached_set::{CacheId, CachedSet},
3+ change_list::ChangeListBuilder,
4+ events::EventsRegistry,
5+ node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
6+};
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::cmp::Ordering;
9+use std::u32;
10+use wasm_bindgen::UnwrapThrowExt;
11+
12+// Diff the `old` node with the `new` node. Emits instructions to modify a
13+// physical DOM node that reflects `old` into something that reflects `new`.
14+//
15+// Upon entry to this function, the physical DOM node must be on the top of the
16+// change list stack:
17+//
18+// [... node]
19+//
20+// The change list stack is in the same state when this function exits.
21+pub(crate) fn diff(
22+ cached_set: &CachedSet,
23+ change_list: &mut ChangeListBuilder,
24+ registry: &mut EventsRegistry,
25+ old: &Node,
26+ new: &Node,
27+ cached_roots: &mut FxHashSet<CacheId>,
28+) {
29+ match (&new.kind, &old.kind) {
30+ (
31+ &NodeKind::Text(TextNode { text: new_text }),
32+ &NodeKind::Text(TextNode { text: old_text }),
33+ ) => {
34+ if new_text != old_text {
35+ change_list.commit_traversal();
36+ change_list.set_text(new_text);
37+ }
38+ }
39+
40+ (&NodeKind::Text(_), &NodeKind::Element(_)) => {
41+ change_list.commit_traversal();
42+ create(cached_set, change_list, registry, new, cached_roots);
43+ registry.remove_subtree(&old);
44+ change_list.replace_with();
45+ }
46+
47+ (&NodeKind::Element(_), &NodeKind::Text(_)) => {
48+ change_list.commit_traversal();
49+ create(cached_set, change_list, registry, new, cached_roots);
50+ // Note: text nodes cannot have event listeners, so we don't need to
51+ // remove the old node's listeners from our registry her.
52+ change_list.replace_with();
53+ }
54+
55+ (
56+ &NodeKind::Element(ElementNode {
57+ key: _,
58+ tag_name: new_tag_name,
59+ listeners: new_listeners,
60+ attributes: new_attributes,
61+ children: new_children,
62+ namespace: new_namespace,
63+ }),
64+ &NodeKind::Element(ElementNode {
65+ key: _,
66+ tag_name: old_tag_name,
67+ listeners: old_listeners,
68+ attributes: old_attributes,
69+ children: old_children,
70+ namespace: old_namespace,
71+ }),
72+ ) => {
73+ if new_tag_name != old_tag_name || new_namespace != old_namespace {
74+ change_list.commit_traversal();
75+ create(cached_set, change_list, registry, new, cached_roots);
76+ registry.remove_subtree(&old);
77+ change_list.replace_with();
78+ return;
79+ }
80+ diff_listeners(change_list, registry, old_listeners, new_listeners);
81+ diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some());
82+ diff_children(
83+ cached_set,
84+ change_list,
85+ registry,
86+ old_children,
87+ new_children,
88+ cached_roots,
89+ );
90+ }
91+
92+ // Both the new and old nodes are cached.
93+ (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => {
94+ cached_roots.insert(new.id);
95+
96+ if new.id == old.id {
97+ // This is the same cached node, so nothing has changed!
98+ return;
99+ }
100+
101+ let (new, new_template) = cached_set.get(new.id);
102+ let (old, old_template) = cached_set.get(old.id);
103+ if new_template == old_template {
104+ // If they are both using the same template, then just diff the
105+ // subtrees.
106+ diff(cached_set, change_list, registry, old, new, cached_roots);
107+ } else {
108+ // Otherwise, they are probably different enough that
109+ // re-constructing the subtree from scratch should be faster.
110+ // This doubly holds true if we have a new template.
111+ change_list.commit_traversal();
112+ create_and_replace(
113+ cached_set,
114+ change_list,
115+ registry,
116+ new_template,
117+ old,
118+ new,
119+ cached_roots,
120+ );
121+ }
122+ }
123+
124+ // New cached node when the old node was not cached. In this scenario,
125+ // we assume that they are pretty different, and it isn't worth diffing
126+ // the subtrees, so we just create the new cached node afresh.
127+ (&NodeKind::Cached(ref c), _) => {
128+ change_list.commit_traversal();
129+ cached_roots.insert(c.id);
130+ let (new, new_template) = cached_set.get(c.id);
131+ create_and_replace(
132+ cached_set,
133+ change_list,
134+ registry,
135+ new_template,
136+ old,
137+ new,
138+ cached_roots,
139+ );
140+ }
141+
142+ // Old cached node and new non-cached node. Again, assume that they are
143+ // probably pretty different and create the new non-cached node afresh.
144+ (_, &NodeKind::Cached(_)) => {
145+ change_list.commit_traversal();
146+ create(cached_set, change_list, registry, new, cached_roots);
147+ registry.remove_subtree(&old);
148+ change_list.replace_with();
149+ }
150+ }
151+}
152+
153+// Diff event listeners between `old` and `new`.
154+//
155+// The listeners' node must be on top of the change list stack:
156+//
157+// [... node]
158+//
159+// The change list stack is left unchanged.
160+fn diff_listeners(
161+ change_list: &mut ChangeListBuilder,
162+ registry: &mut EventsRegistry,
163+ old: &[Listener],
164+ new: &[Listener],
165+) {
166+ if !old.is_empty() || !new.is_empty() {
167+ change_list.commit_traversal();
168+ }
169+
170+ 'outer1: for new_l in new {
171+ unsafe {
172+ // Safety relies on removing `new_l` from the registry manually at
173+ // the end of its lifetime. This happens below in the `'outer2`
174+ // loop, and elsewhere in diffing when removing old dom trees.
175+ registry.add(new_l);
176+ }
177+
178+ for old_l in old {
179+ if new_l.event == old_l.event {
180+ change_list.update_event_listener(new_l);
181+ continue 'outer1;
182+ }
183+ }
184+
185+ change_list.new_event_listener(new_l);
186+ }
187+
188+ 'outer2: for old_l in old {
189+ registry.remove(old_l);
190+
191+ for new_l in new {
192+ if new_l.event == old_l.event {
193+ continue 'outer2;
194+ }
195+ }
196+ change_list.remove_event_listener(old_l.event);
197+ }
198+}
199+
200+// Diff a node's attributes.
201+//
202+// The attributes' node must be on top of the change list stack:
203+//
204+// [... node]
205+//
206+// The change list stack is left unchanged.
207+fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) {
208+ // Do O(n^2) passes to add/update and remove attributes, since
209+ // there are almost always very few attributes.
210+ 'outer: for new_attr in new {
211+ if new_attr.is_volatile() {
212+ change_list.commit_traversal();
213+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
214+ } else {
215+ for old_attr in old {
216+ if old_attr.name == new_attr.name {
217+ if old_attr.value != new_attr.value {
218+ change_list.commit_traversal();
219+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
220+ }
221+ continue 'outer;
222+ }
223+ }
224+
225+ change_list.commit_traversal();
226+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
227+ }
228+ }
229+
230+ 'outer2: for old_attr in old {
231+ for new_attr in new {
232+ if old_attr.name == new_attr.name {
233+ continue 'outer2;
234+ }
235+ }
236+
237+ change_list.commit_traversal();
238+ change_list.remove_attribute(old_attr.name);
239+ }
240+}
241+
242+// Diff the given set of old and new children.
243+//
244+// The parent must be on top of the change list stack when this function is
245+// entered:
246+//
247+// [... parent]
248+//
249+// the change list stack is in the same state when this function returns.
250+fn diff_children(
251+ cached_set: &CachedSet,
252+ change_list: &mut ChangeListBuilder,
253+ registry: &mut EventsRegistry,
254+ old: &[Node],
255+ new: &[Node],
256+ cached_roots: &mut FxHashSet<CacheId>,
257+) {
258+ if new.is_empty() {
259+ if !old.is_empty() {
260+ change_list.commit_traversal();
261+ remove_all_children(change_list, registry, old);
262+ }
263+ return;
264+ }
265+
266+ if new.len() == 1 {
267+ match (old.first(), &new[0]) {
268+ (
269+ Some(&Node {
270+ kind: NodeKind::Text(TextNode { text: old_text }),
271+ }),
272+ &Node {
273+ kind: NodeKind::Text(TextNode { text: new_text }),
274+ },
275+ ) if old_text == new_text => {
276+ // Don't take this fast path...
277+ }
278+ (
279+ _,
280+ &Node {
281+ kind: NodeKind::Text(TextNode { text }),
282+ },
283+ ) => {
284+ change_list.commit_traversal();
285+ change_list.set_text(text);
286+ for o in old {
287+ registry.remove_subtree(o);
288+ }
289+ return;
290+ }
291+ (_, _) => {}
292+ }
293+ }
294+
295+ if old.is_empty() {
296+ if !new.is_empty() {
297+ change_list.commit_traversal();
298+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
299+ }
300+ return;
301+ }
302+
303+ let new_is_keyed = new[0].key().is_some();
304+ let old_is_keyed = old[0].key().is_some();
305+
306+ debug_assert!(
307+ new.iter().all(|n| n.key().is_some() == new_is_keyed),
308+ "all siblings must be keyed or all siblings must be non-keyed"
309+ );
310+ debug_assert!(
311+ old.iter().all(|o| o.key().is_some() == old_is_keyed),
312+ "all siblings must be keyed or all siblings must be non-keyed"
313+ );
314+
315+ if new_is_keyed && old_is_keyed {
316+ let t = change_list.next_temporary();
317+ diff_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
318+ change_list.set_next_temporary(t);
319+ } else {
320+ diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
321+ }
322+}
323+
324+// Diffing "keyed" children.
325+//
326+// With keyed children, we care about whether we delete, move, or create nodes
327+// versus mutate existing nodes in place. Presumably there is some sort of CSS
328+// transition animation that makes the virtual DOM diffing algorithm
329+// observable. By specifying keys for nodes, we know which virtual DOM nodes
330+// must reuse (or not reuse) the same physical DOM nodes.
331+//
332+// This is loosely based on Inferno's keyed patching implementation. However, we
333+// have to modify the algorithm since we are compiling the diff down into change
334+// list instructions that will be executed later, rather than applying the
335+// changes to the DOM directly as we compare virtual DOMs.
336+//
337+// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
338+//
339+// When entering this function, the parent must be on top of the change list
340+// stack:
341+//
342+// [... parent]
343+//
344+// Upon exiting, the change list stack is in the same state.
345+fn diff_keyed_children(
346+ cached_set: &CachedSet,
347+ change_list: &mut ChangeListBuilder,
348+ registry: &mut EventsRegistry,
349+ old: &[Node],
350+ new: &[Node],
351+ cached_roots: &mut FxHashSet<CacheId>,
352+) {
353+ if cfg!(debug_assertions) {
354+ let mut keys = FxHashSet::default();
355+ let mut assert_unique_keys = |children: &[Node]| {
356+ keys.clear();
357+ for child in children {
358+ let key = child.key();
359+ debug_assert!(
360+ key.is_some(),
361+ "if any sibling is keyed, all siblings must be keyed"
362+ );
363+ keys.insert(key);
364+ }
365+ debug_assert_eq!(
366+ children.len(),
367+ keys.len(),
368+ "keyed siblings must each have a unique key"
369+ );
370+ };
371+ assert_unique_keys(old);
372+ assert_unique_keys(new);
373+ }
374+
375+ // First up, we diff all the nodes with the same key at the beginning of the
376+ // children.
377+ //
378+ // `shared_prefix_count` is the count of how many nodes at the start of
379+ // `new` and `old` share the same keys.
380+ let shared_prefix_count =
381+ match diff_keyed_prefix(cached_set, change_list, registry, old, new, cached_roots) {
382+ KeyedPrefixResult::Finished => return,
383+ KeyedPrefixResult::MoreWorkToDo(count) => count,
384+ };
385+
386+ // Next, we find out how many of the nodes at the end of the children have
387+ // the same key. We do _not_ diff them yet, since we want to emit the change
388+ // list instructions such that they can be applied in a single pass over the
389+ // DOM. Instead, we just save this information for later.
390+ //
391+ // `shared_suffix_count` is the count of how many nodes at the end of `new`
392+ // and `old` share the same keys.
393+ let shared_suffix_count = old[shared_prefix_count..]
394+ .iter()
395+ .rev()
396+ .zip(new[shared_prefix_count..].iter().rev())
397+ .take_while(|&(old, new)| old.key() == new.key())
398+ .count();
399+
400+ let old_shared_suffix_start = old.len() - shared_suffix_count;
401+ let new_shared_suffix_start = new.len() - shared_suffix_count;
402+
403+ // Ok, we now hopefully have a smaller range of children in the middle
404+ // within which to re-order nodes with the same keys, remove old nodes with
405+ // now-unused keys, and create new nodes with fresh keys.
406+ diff_keyed_middle(
407+ cached_set,
408+ change_list,
409+ registry,
410+ &old[shared_prefix_count..old_shared_suffix_start],
411+ &new[shared_prefix_count..new_shared_suffix_start],
412+ cached_roots,
413+ shared_prefix_count,
414+ shared_suffix_count,
415+ old_shared_suffix_start,
416+ );
417+
418+ // Finally, diff the nodes at the end of `old` and `new` that share keys.
419+ let old_suffix = &old[old_shared_suffix_start..];
420+ let new_suffix = &new[new_shared_suffix_start..];
421+ debug_assert_eq!(old_suffix.len(), new_suffix.len());
422+ if !old_suffix.is_empty() {
423+ diff_keyed_suffix(
424+ cached_set,
425+ change_list,
426+ registry,
427+ old_suffix,
428+ new_suffix,
429+ cached_roots,
430+ new_shared_suffix_start,
431+ );
432+ }
433+}
434+
435+enum KeyedPrefixResult {
436+ // Fast path: we finished diffing all the children just by looking at the
437+ // prefix of shared keys!
438+ Finished,
439+ // There is more diffing work to do. Here is a count of how many children at
440+ // the beginning of `new` and `old` we already processed.
441+ MoreWorkToDo(usize),
442+}
443+
444+// Diff the prefix of children in `new` and `old` that share the same keys in
445+// the same order.
446+//
447+// Upon entry of this function, the change list stack must be:
448+//
449+// [... parent]
450+//
451+// Upon exit, the change list stack is the same.
452+fn diff_keyed_prefix(
453+ cached_set: &CachedSet,
454+ change_list: &mut ChangeListBuilder,
455+ registry: &mut EventsRegistry,
456+ old: &[Node],
457+ new: &[Node],
458+ cached_roots: &mut FxHashSet<CacheId>,
459+) -> KeyedPrefixResult {
460+ change_list.go_down();
461+ let mut shared_prefix_count = 0;
462+
463+ for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
464+ if old.key() != new.key() {
465+ break;
466+ }
467+
468+ change_list.go_to_sibling(i);
469+ diff(cached_set, change_list, registry, old, new, cached_roots);
470+ shared_prefix_count += 1;
471+ }
472+
473+ // If that was all of the old children, then create and append the remaining
474+ // new children and we're finished.
475+ if shared_prefix_count == old.len() {
476+ change_list.go_up();
477+ change_list.commit_traversal();
478+ create_and_append_children(
479+ cached_set,
480+ change_list,
481+ registry,
482+ &new[shared_prefix_count..],
483+ cached_roots,
484+ );
485+ return KeyedPrefixResult::Finished;
486+ }
487+
488+ // And if that was all of the new children, then remove all of the remaining
489+ // old children and we're finished.
490+ if shared_prefix_count == new.len() {
491+ change_list.go_to_sibling(shared_prefix_count);
492+ change_list.commit_traversal();
493+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
494+ return KeyedPrefixResult::Finished;
495+ }
496+
497+ change_list.go_up();
498+ KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
499+}
500+
501+// The most-general, expensive code path for keyed children diffing.
502+//
503+// We find the longest subsequence within `old` of children that are relatively
504+// ordered the same way in `new` (via finding a longest-increasing-subsequence
505+// of the old child's index within `new`). The children that are elements of
506+// this subsequence will remain in place, minimizing the number of DOM moves we
507+// will have to do.
508+//
509+// Upon entry to this function, the change list stack must be:
510+//
511+// [... parent]
512+//
513+// Upon exit from this function, it will be restored to that same state.
514+fn diff_keyed_middle(
515+ cached_set: &CachedSet,
516+ change_list: &mut ChangeListBuilder,
517+ registry: &mut EventsRegistry,
518+ old: &[Node],
519+ mut new: &[Node],
520+ cached_roots: &mut FxHashSet<CacheId>,
521+ shared_prefix_count: usize,
522+ shared_suffix_count: usize,
523+ old_shared_suffix_start: usize,
524+) {
525+ // Should have already diffed the shared-key prefixes and suffixes.
526+ debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
527+ debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
528+
529+ // The algorithm below relies upon using `u32::MAX` as a sentinel
530+ // value, so if we have that many new nodes, it won't work. This
531+ // check is a bit academic (hence only enabled in debug), since
532+ // wasm32 doesn't have enough address space to hold that many nodes
533+ // in memory.
534+ debug_assert!(new.len() < u32::MAX as usize);
535+
536+ // Map from each `old` node's key to its index within `old`.
537+ let mut old_key_to_old_index = FxHashMap::default();
538+ old_key_to_old_index.reserve(old.len());
539+ old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
540+
541+ // The set of shared keys between `new` and `old`.
542+ let mut shared_keys = FxHashSet::default();
543+ // Map from each index in `new` to the index of the node in `old` that
544+ // has the same key.
545+ let mut new_index_to_old_index = Vec::with_capacity(new.len());
546+ new_index_to_old_index.extend(new.iter().map(|n| {
547+ let key = n.key();
548+ if let Some(&i) = old_key_to_old_index.get(&key) {
549+ shared_keys.insert(key);
550+ i
551+ } else {
552+ u32::MAX as usize
553+ }
554+ }));
555+
556+ // If none of the old keys are reused by the new children, then we
557+ // remove all the remaining old children and create the new children
558+ // afresh.
559+ if shared_suffix_count == 0 && shared_keys.is_empty() {
560+ if shared_prefix_count == 0 {
561+ change_list.commit_traversal();
562+ remove_all_children(change_list, registry, old);
563+ } else {
564+ change_list.go_down_to_child(shared_prefix_count);
565+ change_list.commit_traversal();
566+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
567+ }
568+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
569+ return;
570+ }
571+
572+ // Save each of the old children whose keys are reused in the new
573+ // children.
574+ let mut old_index_to_temp = vec![u32::MAX; old.len()];
575+ let mut start = 0;
576+ loop {
577+ let end = (start..old.len())
578+ .find(|&i| {
579+ let key = old[i].key();
580+ !shared_keys.contains(&key)
581+ })
582+ .unwrap_or(old.len());
583+
584+ if end - start > 0 {
585+ change_list.commit_traversal();
586+ let mut t = change_list.save_children_to_temporaries(
587+ shared_prefix_count + start,
588+ shared_prefix_count + end,
589+ );
590+ for i in start..end {
591+ old_index_to_temp[i] = t;
592+ t += 1;
593+ }
594+ }
595+
596+ debug_assert!(end <= old.len());
597+ if end == old.len() {
598+ break;
599+ } else {
600+ start = end + 1;
601+ }
602+ }
603+
604+ // Remove any old children whose keys were not reused in the new
605+ // children. Remove from the end first so that we don't mess up indices.
606+ let mut removed_count = 0;
607+ for (i, old_child) in old.iter().enumerate().rev() {
608+ if !shared_keys.contains(&old_child.key()) {
609+ registry.remove_subtree(old_child);
610+ change_list.commit_traversal();
611+ change_list.remove_child(i + shared_prefix_count);
612+ removed_count += 1;
613+ }
614+ }
615+
616+ // If there aren't any more new children, then we are done!
617+ if new.is_empty() {
618+ return;
619+ }
620+
621+ // The longest increasing subsequence within `new_index_to_old_index`. This
622+ // is the longest sequence on DOM nodes in `old` that are relatively ordered
623+ // correctly within `new`. We will leave these nodes in place in the DOM,
624+ // and only move nodes that are not part of the LIS. This results in the
625+ // maximum number of DOM nodes left in place, AKA the minimum number of DOM
626+ // nodes moved.
627+ let mut new_index_is_in_lis = FxHashSet::default();
628+ new_index_is_in_lis.reserve(new_index_to_old_index.len());
629+ let mut predecessors = vec![0; new_index_to_old_index.len()];
630+ let mut starts = vec![0; new_index_to_old_index.len()];
631+ longest_increasing_subsequence::lis_with(
632+ &new_index_to_old_index,
633+ &mut new_index_is_in_lis,
634+ |a, b| a < b,
635+ &mut predecessors,
636+ &mut starts,
637+ );
638+
639+ // Now we will iterate from the end of the new children back to the
640+ // beginning, diffing old children we are reusing and if they aren't in the
641+ // LIS moving them to their new destination, or creating new children. Note
642+ // that iterating in reverse order lets us use `Node.prototype.insertBefore`
643+ // to move/insert children.
644+ //
645+ // But first, we ensure that we have a child on the change list stack that
646+ // we can `insertBefore`. We handle this once before looping over `new`
647+ // children, so that we don't have to keep checking on every loop iteration.
648+ if shared_suffix_count > 0 {
649+ // There is a shared suffix after these middle children. We will be
650+ // inserting before that shared suffix, so add the first child of that
651+ // shared suffix to the change list stack.
652+ //
653+ // [... parent]
654+ change_list.go_down_to_child(old_shared_suffix_start - removed_count);
655+ // [... parent first_child_of_shared_suffix]
656+ } else {
657+ // There is no shared suffix coming after these middle children.
658+ // Therefore we have to process the last child in `new` and move it to
659+ // the end of the parent's children if it isn't already there.
660+ let last_index = new.len() - 1;
661+ let last = new.last().unwrap_throw();
662+ new = &new[..new.len() - 1];
663+ if shared_keys.contains(&last.key()) {
664+ let old_index = new_index_to_old_index[last_index];
665+ let temp = old_index_to_temp[old_index];
666+ // [... parent]
667+ change_list.go_down_to_temp_child(temp);
668+ // [... parent last]
669+ diff(
670+ cached_set,
671+ change_list,
672+ registry,
673+ &old[old_index],
674+ last,
675+ cached_roots,
676+ );
677+ if new_index_is_in_lis.contains(&last_index) {
678+ // Don't move it, since it is already where it needs to be.
679+ } else {
680+ change_list.commit_traversal();
681+ // [... parent last]
682+ change_list.append_child();
683+ // [... parent]
684+ change_list.go_down_to_temp_child(temp);
685+ // [... parent last]
686+ }
687+ } else {
688+ change_list.commit_traversal();
689+ // [... parent]
690+ create(cached_set, change_list, registry, last, cached_roots);
691+ // [... parent last]
692+ change_list.append_child();
693+ // [... parent]
694+ change_list.go_down_to_reverse_child(0);
695+ // [... parent last]
696+ }
697+ }
698+
699+ for (new_index, new_child) in new.iter().enumerate().rev() {
700+ let old_index = new_index_to_old_index[new_index];
701+ if old_index == u32::MAX as usize {
702+ debug_assert!(!shared_keys.contains(&new_child.key()));
703+ change_list.commit_traversal();
704+ // [... parent successor]
705+ create(cached_set, change_list, registry, new_child, cached_roots);
706+ // [... parent successor new_child]
707+ change_list.insert_before();
708+ // [... parent new_child]
709+ } else {
710+ debug_assert!(shared_keys.contains(&new_child.key()));
711+ let temp = old_index_to_temp[old_index];
712+ debug_assert_ne!(temp, u32::MAX);
713+
714+ if new_index_is_in_lis.contains(&new_index) {
715+ // [... parent successor]
716+ change_list.go_to_temp_sibling(temp);
717+ // [... parent new_child]
718+ } else {
719+ change_list.commit_traversal();
720+ // [... parent successor]
721+ change_list.push_temporary(temp);
722+ // [... parent successor new_child]
723+ change_list.insert_before();
724+ // [... parent new_child]
725+ }
726+
727+ diff(
728+ cached_set,
729+ change_list,
730+ registry,
731+ &old[old_index],
732+ new_child,
733+ cached_roots,
734+ );
735+ }
736+ }
737+
738+ // [... parent child]
739+ change_list.go_up();
740+ // [... parent]
741+}
742+
743+// Diff the suffix of keyed children that share the same keys in the same order.
744+//
745+// The parent must be on the change list stack when we enter this function:
746+//
747+// [... parent]
748+//
749+// When this function exits, the change list stack remains the same.
750+fn diff_keyed_suffix(
751+ cached_set: &CachedSet,
752+ change_list: &mut ChangeListBuilder,
753+ registry: &mut EventsRegistry,
754+ old: &[Node],
755+ new: &[Node],
756+ cached_roots: &mut FxHashSet<CacheId>,
757+ new_shared_suffix_start: usize,
758+) {
759+ debug_assert_eq!(old.len(), new.len());
760+ debug_assert!(!old.is_empty());
761+
762+ // [... parent]
763+ change_list.go_down();
764+ // [... parent new_child]
765+
766+ for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
767+ change_list.go_to_sibling(new_shared_suffix_start + i);
768+ diff(
769+ cached_set,
770+ change_list,
771+ registry,
772+ old_child,
773+ new_child,
774+ cached_roots,
775+ );
776+ }
777+
778+ // [... parent]
779+ change_list.go_up();
780+}
781+
782+// Diff children that are not keyed.
783+//
784+// The parent must be on the top of the change list stack when entering this
785+// function:
786+//
787+// [... parent]
788+//
789+// the change list stack is in the same state when this function returns.
790+fn diff_non_keyed_children(
791+ cached_set: &CachedSet,
792+ change_list: &mut ChangeListBuilder,
793+ registry: &mut EventsRegistry,
794+ old: &[Node],
795+ new: &[Node],
796+ cached_roots: &mut FxHashSet<CacheId>,
797+) {
798+ // Handled these cases in `diff_children` before calling this function.
799+ debug_assert!(!new.is_empty());
800+ debug_assert!(!old.is_empty());
801+
802+ // [... parent]
803+ change_list.go_down();
804+ // [... parent child]
805+
806+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
807+ // [... parent prev_child]
808+ change_list.go_to_sibling(i);
809+ // [... parent this_child]
810+ diff(
811+ cached_set,
812+ change_list,
813+ registry,
814+ old_child,
815+ new_child,
816+ cached_roots,
817+ );
818+ }
819+
820+ match old.len().cmp(&new.len()) {
821+ Ordering::Greater => {
822+ // [... parent prev_child]
823+ change_list.go_to_sibling(new.len());
824+ // [... parent first_child_to_remove]
825+ change_list.commit_traversal();
826+ remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
827+ // [... parent]
828+ }
829+ Ordering::Less => {
830+ // [... parent last_child]
831+ change_list.go_up();
832+ // [... parent]
833+ change_list.commit_traversal();
834+ create_and_append_children(
835+ cached_set,
836+ change_list,
837+ registry,
838+ &new[old.len()..],
839+ cached_roots,
840+ );
841+ }
842+ Ordering::Equal => {
843+ // [... parent child]
844+ change_list.go_up();
845+ // [... parent]
846+ }
847+ }
848+}
849+
850+// Create the given children and append them to the parent node.
851+//
852+// The parent node must currently be on top of the change list stack:
853+//
854+// [... parent]
855+//
856+// When this function returns, the change list stack is in the same state.
857+fn create_and_append_children(
858+ cached_set: &CachedSet,
859+ change_list: &mut ChangeListBuilder,
860+ registry: &mut EventsRegistry,
861+ new: &[Node],
862+ cached_roots: &mut FxHashSet<CacheId>,
863+) {
864+ debug_assert!(change_list.traversal_is_committed());
865+ for child in new {
866+ create(cached_set, change_list, registry, child, cached_roots);
867+ change_list.append_child();
868+ }
869+}
870+
871+// Remove all of a node's children.
872+//
873+// The change list stack must have this shape upon entry to this function:
874+//
875+// [... parent]
876+//
877+// When this function returns, the change list stack is in the same state.
878+fn remove_all_children(
879+ change_list: &mut ChangeListBuilder,
880+ registry: &mut EventsRegistry,
881+ old: &[Node],
882+) {
883+ debug_assert!(change_list.traversal_is_committed());
884+ for child in old {
885+ registry.remove_subtree(child);
886+ }
887+ // Fast way to remove all children: set the node's textContent to an empty
888+ // string.
889+ change_list.set_text("");
890+}
891+
892+// Remove the current child and all of its following siblings.
893+//
894+// The change list stack must have this shape upon entry to this function:
895+//
896+// [... parent child]
897+//
898+// After the function returns, the child is no longer on the change list stack:
899+//
900+// [... parent]
901+fn remove_self_and_next_siblings(
902+ change_list: &mut ChangeListBuilder,
903+ registry: &mut EventsRegistry,
904+ old: &[Node],
905+) {
906+ debug_assert!(change_list.traversal_is_committed());
907+ for child in old {
908+ registry.remove_subtree(child);
909+ }
910+ change_list.remove_self_and_next_siblings();
911+}
912+
913+// Emit instructions to create the given virtual node.
914+//
915+// The change list stack may have any shape upon entering this function:
916+//
917+// [...]
918+//
919+// When this function returns, the new node is on top of the change list stack:
920+//
921+// [... node]
922+fn create(
923+ cached_set: &CachedSet,
924+ change_list: &mut ChangeListBuilder,
925+ registry: &mut EventsRegistry,
926+ node: &Node,
927+ cached_roots: &mut FxHashSet<CacheId>,
928+) {
929+ debug_assert!(change_list.traversal_is_committed());
930+ match node.kind {
931+ NodeKind::Text(TextNode { text }) => {
932+ change_list.create_text_node(text);
933+ }
934+ NodeKind::Element(&ElementNode {
935+ key: _,
936+ tag_name,
937+ listeners,
938+ attributes,
939+ children,
940+ namespace,
941+ }) => {
942+ if let Some(namespace) = namespace {
943+ change_list.create_element_ns(tag_name, namespace);
944+ } else {
945+ change_list.create_element(tag_name);
946+ }
947+
948+ for l in listeners {
949+ unsafe {
950+ registry.add(l);
951+ }
952+ change_list.new_event_listener(l);
953+ }
954+
955+ for attr in attributes {
956+ change_list.set_attribute(&attr.name, &attr.value, namespace.is_some());
957+ }
958+
959+ // Fast path: if there is a single text child, it is faster to
960+ // create-and-append the text node all at once via setting the
961+ // parent's `textContent` in a single change list instruction than
962+ // to emit three instructions to (1) create a text node, (2) set its
963+ // text content, and finally (3) append the text node to this
964+ // parent.
965+ if children.len() == 1 {
966+ if let Node {
967+ kind: NodeKind::Text(TextNode { text }),
968+ } = children[0]
969+ {
970+ change_list.set_text(text);
971+ return;
972+ }
973+ }
974+
975+ for child in children {
976+ create(cached_set, change_list, registry, child, cached_roots);
977+ change_list.append_child();
978+ }
979+ }
980+ NodeKind::Cached(ref c) => {
981+ cached_roots.insert(c.id);
982+ let (node, template) = cached_set.get(c.id);
983+ if let Some(template) = template {
984+ create_with_template(
985+ cached_set,
986+ change_list,
987+ registry,
988+ template,
989+ node,
990+ cached_roots,
991+ );
992+ } else {
993+ create(cached_set, change_list, registry, node, cached_roots);
994+ }
995+ }
996+ }
997+}
998+
999+// Get or create the template.
1000+//
1001+// Upon entering this function the change list stack may be in any shape:
1002+//
1003+// [...]
1004+//
1005+// When this function returns, it leaves a freshly cloned copy of the template
1006+// on the top of the change list stack:
1007+//
1008+// [... template]
1009+#[inline]
1010+fn get_or_create_template<'a>(
1011+ cached_set: &'a CachedSet,
1012+ change_list: &mut ChangeListBuilder,
1013+ registry: &mut EventsRegistry,
1014+ cached_roots: &mut FxHashSet<CacheId>,
1015+ template_id: CacheId,
1016+) -> (&'a Node<'a>, bool) {
1017+ let (template, template_template) = cached_set.get(template_id);
1018+ debug_assert!(
1019+ template_template.is_none(),
1020+ "templates should not be templated themselves"
1021+ );
1022+
1023+ // If we haven't already created and saved the physical DOM subtree for this
1024+ // template, do that now.
1025+ if change_list.has_template(template_id) {
1026+ // Clone the template and push it onto the stack.
1027+ //
1028+ // [...]
1029+ change_list.push_template(template_id);
1030+ // [... template]
1031+
1032+ (template, true)
1033+ } else {
1034+ // [...]
1035+ create(cached_set, change_list, registry, template, cached_roots);
1036+ // [... template]
1037+ change_list.save_template(template_id);
1038+ // [... template]
1039+
1040+ (template, false)
1041+ }
1042+}
1043+
1044+fn create_and_replace(
1045+ cached_set: &CachedSet,
1046+ change_list: &mut ChangeListBuilder,
1047+ registry: &mut EventsRegistry,
1048+ new_template: Option<CacheId>,
1049+ old: &Node,
1050+ new: &Node,
1051+ cached_roots: &mut FxHashSet<CacheId>,
1052+) {
1053+ debug_assert!(change_list.traversal_is_committed());
1054+
1055+ if let Some(template_id) = new_template {
1056+ let (template, needs_listeners) =
1057+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1058+ change_list.replace_with();
1059+
1060+ let mut old_forcing = None;
1061+ if needs_listeners {
1062+ old_forcing = Some(change_list.push_force_new_listeners());
1063+ }
1064+
1065+ diff(
1066+ cached_set,
1067+ change_list,
1068+ registry,
1069+ template,
1070+ new,
1071+ cached_roots,
1072+ );
1073+
1074+ if let Some(old) = old_forcing {
1075+ change_list.pop_force_new_listeners(old);
1076+ }
1077+
1078+ change_list.commit_traversal();
1079+ } else {
1080+ create(cached_set, change_list, registry, new, cached_roots);
1081+ change_list.replace_with();
1082+ }
1083+ registry.remove_subtree(old);
1084+}
1085+
1086+fn create_with_template(
1087+ cached_set: &CachedSet,
1088+ change_list: &mut ChangeListBuilder,
1089+ registry: &mut EventsRegistry,
1090+ template_id: CacheId,
1091+ node: &Node,
1092+ cached_roots: &mut FxHashSet<CacheId>,
1093+) {
1094+ debug_assert!(change_list.traversal_is_committed());
1095+
1096+ // [...]
1097+ let (template, needs_listeners) =
1098+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1099+ // [... template]
1100+
1101+ // Now diff the node with its template.
1102+ //
1103+ // We must force adding new listeners instead of updating existing ones,
1104+ // since listeners don't get cloned in `cloneNode`.
1105+ let mut old_forcing = None;
1106+ if needs_listeners {
1107+ old_forcing = Some(change_list.push_force_new_listeners());
1108+ }
1109+
1110+ diff(
1111+ cached_set,
1112+ change_list,
1113+ registry,
1114+ template,
1115+ node,
1116+ cached_roots,
1117+ );
1118+
1119+ if let Some(old) = old_forcing {
1120+ change_list.pop_force_new_listeners(old);
1121+ }
1122+
1123+ // Make sure that we come back up to the level we were at originally.
1124+ change_list.commit_traversal();
1125+}
src/events.rs+123−83
@@ -1,98 +1,138 @@
1−use crate::vdom::{VdomInner, VdomWeak};
2−use crate::{Listener, ListenerCallback};
1+use crate::{node::Node, vdom::VdomInner, Listener};
32use std::cell::RefCell;
4−use std::collections::HashMap;
5−use std::fmt;
6−use std::mem;
73use std::rc::{Rc, Weak};
8−use wasm_bindgen::closure::Closure;
9−use wasm_bindgen::prelude::*;
104
11−/// The events registry manages event listeners for a virtual DOM.
12−///
13−/// The events registry is persistent across virtual DOM rendering and double
14−/// buffering.
15−pub(crate) struct EventsRegistry {
16− vdom: Weak<VdomInner>,
17− active: HashMap<(u32, u32), ListenerCallback<'static>>,
18−}
5+cfg_if::cfg_if! {
6+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
7+ #[derive(Debug)]
8+ pub(crate) struct EventsRegistry {}
9+ impl EventsRegistry {
10+ pub(crate) fn new(_vdom: Weak<VdomInner>) -> (
11+ Rc<RefCell<EventsRegistry>>,
12+ crate::EventsTrampoline,
13+ ) {
14+ (Rc::new(RefCell::new(EventsRegistry {})), ())
15+ }
16+ pub(crate) fn remove(&mut self, _listener: &Listener) {}
17+ pub(crate) fn remove_subtree(&mut self, _node: &Node) {}
18+ pub(crate) unsafe fn add<'a>(&mut self, _listener: &'a Listener<'a>) {}
19+ pub(crate) fn clear_active_listeners(&mut self) {}
20+ }
21+ } else {
22+ use crate::{
23+ node::{ElementNode, ListenerCallback, NodeKind},
24+ vdom::VdomWeak,
25+ };
26+ use fxhash::FxHashMap;
27+ use std::fmt;
28+ use std::mem;
29+ use wasm_bindgen::closure::Closure;
30+ use wasm_bindgen::prelude::*;
1931
20−impl fmt::Debug for EventsRegistry {
21− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22− f.debug_struct("EventsRegistry")
23− .field("active", &self.active.keys().collect::<Vec<_>>())
24− .finish()
25− }
26−}
32+ /// The events registry manages event listeners for a virtual DOM.
33+ ///
34+ /// The events registry is persistent across virtual DOM rendering and double
35+ /// buffering.
36+ pub(crate) struct EventsRegistry {
37+ vdom: Weak<VdomInner>,
38+ active: FxHashMap<(u32, u32), ListenerCallback<'static>>,
39+ }
2740
28−impl EventsRegistry {
29− /// Construct a new events registry and JS function trampoline that weakly
30− /// holds the new registry and can be used by JS to invoke listeners on the
31− /// Rust side.
32− pub(crate) fn new(
33− vdom: Weak<VdomInner>,
34− ) -> (
35− Rc<RefCell<EventsRegistry>>,
36− Closure<Fn(web_sys::Event, u32, u32)>,
37− ) {
38− let registry = Rc::new(RefCell::new(EventsRegistry {
39− vdom,
40− active: HashMap::new(),
41− }));
41+ impl fmt::Debug for EventsRegistry {
42+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43+ f.debug_struct("EventsRegistry")
44+ .field("active", &self.active.keys().collect::<Vec<_>>())
45+ .finish()
46+ }
47+ }
4248
43− let weak_registry = Rc::downgrade(&registry);
44− let closure = Closure::wrap(Box::new(move |event, a, b| {
45− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
46− debug_assert!(a != 0);
49+ impl EventsRegistry {
50+ /// Construct a new events registry and JS function trampoline that weakly
51+ /// holds the new registry and can be used by JS to invoke listeners on the
52+ /// Rust side.
53+ pub(crate) fn new(
54+ vdom: Weak<VdomInner>,
55+ ) -> (
56+ Rc<RefCell<EventsRegistry>>,
57+ crate::EventsTrampoline,
58+ ) {
59+ let registry = Rc::new(RefCell::new(EventsRegistry {
60+ vdom,
61+ active: FxHashMap::default(),
62+ }));
4763
48− // if the VdomInnerExclusive is keeping this closure alive, then the
49− // VdomInnerExclusive should also be keeping the registry alive
50− let registry = weak_registry.upgrade().unwrap_throw();
51− let registry = registry.borrow();
64+ let weak_registry = Rc::downgrade(&registry);
65+ let closure = Closure::wrap(Box::new(move |event, a, b| {
66+ debug_assert!(a != 0);
5267
53− match registry.active.get(&(a, b)) {
54− None => warn!(
55− "EventsRegistry closure invoked with unknown listener parts: \
56− (0x{:x}, 0x{:x})",
57− a, b
58− ),
59− Some(callback) => {
60− let vdom = registry.vdom.upgrade().expect_throw(
61− "if the registry is still around, then the vdom should still be around",
62− );
63− let vdom_weak = VdomWeak::new(&vdom);
64− let mut vdom = vdom.exclusive.borrow_mut();
65− let component = vdom.component_raw_mut();
66− callback(component, vdom_weak, event);
67− }
68+ // if the VdomInnerExclusive is keeping this closure alive, then the
69+ // VdomInnerExclusive should also be keeping the registry alive
70+ let registry = weak_registry.upgrade().unwrap_throw();
71+ let registry = registry.borrow();
72+
73+ match registry.active.get(&(a, b)) {
74+ None => warn!(
75+ "EventsRegistry closure invoked with unknown listener parts: \
76+ (0x{:x}, 0x{:x})",
77+ a, b
78+ ),
79+ Some(callback) => {
80+ let vdom = registry.vdom.upgrade().expect_throw(
81+ "if the registry is still around, then the vdom should still be around",
82+ );
83+ let vdom_weak = VdomWeak::new(&vdom);
84+ let mut vdom = vdom.exclusive.borrow_mut();
85+ let component = vdom.component_raw_mut();
86+ callback(component, vdom_weak, event);
87+ }
88+ }
89+ }) as Box<dyn Fn(web_sys::Event, u32, u32)>);
90+
91+ (registry, closure)
6892 }
69− }) as Box<Fn(web_sys::Event, u32, u32)>);
7093
71− (registry, closure)
72− }
94+ pub(crate) fn remove(&mut self, listener: &Listener) {
95+ let id = listener.get_callback_parts();
96+ debug_assert!(id.0 != 0);
97+ self.active.remove(&id);
98+ }
99+
100+ pub(crate) fn remove_subtree(&mut self, node: &Node) {
101+ match node.kind {
102+ NodeKind::Cached(_) | NodeKind::Text(_) => {},
103+ NodeKind::Element(&ElementNode {listeners, children, ..}) => {
104+ for l in listeners {
105+ self.remove(l);
106+ }
107+ for child in children {
108+ self.remove_subtree(child)
109+ }
110+ }
111+ }
112+ }
73113
74− /// Add an event listener to the registry, exposing to JS.
75− ///
76− /// # Unsafety
77− ///
78− /// The listener's lifetime is extended to `'static` and it is the caller's
79− /// responsibility to ensure that the listener is not kept in the registry
80− /// after it is dropped. This is typically done via clearing the whole
81− /// registry on each render and repopulating it during diffing.
82− pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
83− let id = listener.get_callback_parts();
84− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
85− debug_assert!(id.0 != 0);
114+ /// Add an event listener to the registry, exposing to JS.
115+ ///
116+ /// # Unsafety
117+ ///
118+ /// The listener's lifetime is extended to `'static` and it is the
119+ /// caller's responsibility to ensure that the listener is not kept
120+ /// in the registry after it is dropped. This is maintained during
121+ /// diffing.
122+ pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
123+ let id = listener.get_callback_parts();
124+ debug_assert!(id.0 != 0);
86125
87− let callback =
88− mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
89− let old = self.active.insert(id, callback);
90− debug_assert!(old.is_none());
91− }
126+ let callback =
127+ mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
128+ let old = self.active.insert(id, callback);
129+ debug_assert!(old.is_none());
130+ }
92131
93− /// Clear all event listeners from the registry.
94− pub(crate) fn clear_active_listeners(&mut self) {
95− debug!("EventsRegistry::clear_active_listeners()");
96− self.active.clear();
132+ /// Clear all event listeners from the registry.
133+ pub(crate) fn clear_active_listeners(&mut self) {
134+ self.active.clear();
135+ }
136+ }
97137 }
98138}
src/lib.rs+64−11
@@ -3,7 +3,7 @@
33//! ## Example
44//!
55//! ```no_run
6−//! use dodrio::{bumpalo::Bump, Attribute, Node, Render};
6+//! use dodrio::{bumpalo, Attribute, Node, Render, RenderContext};
77//! use wasm_bindgen::UnwrapThrowExt;
88//!
99//! /// A component that greets someone.
@@ -18,16 +18,14 @@
1818//! }
1919//! }
2020//!
21−//! impl<'who> Render for Hello<'who> {
22−//! fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
23−//! where
24−//! 'a: 'bump,
25−//! {
21+//! impl<'a, 'who> Render<'a> for Hello<'who> {
22+//! fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
2623//! use dodrio::builder::*;
2724//!
28−//! let id = bumpalo::format!(in bump, "hello-{}", self.who);
25+//! let id = bumpalo::format!(in cx.bump, "hello-{}", self.who);
26+//! let who = bumpalo::collections::String::from_str_in(self.who, cx.bump).into_bump_str();
2927//!
30−//! div(bump)
28+//! div(&cx)
3129//! .attr("id", id.into_bump_str())
3230//! .on("click", |root, _vdom, _event| {
3331//! let hello = root.unwrap_mut::<Hello>();
@@ -37,9 +35,9 @@
3735//! })
3836//! .children([
3937//! text("Hello, "),
40−//! strong(bump)
38+//! strong(&cx)
4139//! .children([
42−//! text(self.who),
40+//! text(who),
4341//! text("!"),
4442//! ])
4543//! .finish(),
@@ -63,20 +61,75 @@ cfg_if::cfg_if! {
6361 }
6462}
6563
64+// A macro to expose items unstably, only for internal/testing usage.
65+cfg_if::cfg_if! {
66+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
67+ macro_rules! pub_unstable_internal {
68+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
69+ #[doc(hidden)]
70+ $( #[$attr] )*
71+ pub $( $thing )*
72+ }
73+ }
74+ } else {
75+ macro_rules! pub_unstable_internal {
76+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
77+ $( #[$attr] )*
78+ pub(crate) $( $thing )*
79+ }
80+ }
81+ }
82+}
83+
6684// Only `pub` so that the wasm-bindgen bindings work.
6785#[doc(hidden)]
6886pub mod change_list;
6987
7088mod cached;
89+mod cached_set;
90+mod diff;
7191mod events;
7292mod node;
7393mod render;
94+mod render_context;
95+mod strace;
7496mod vdom;
7597
7698pub mod builder;
7799
78100// Re-export items at the top level.
79101pub use self::cached::Cached;
80−pub use self::node::{Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
102+pub use self::node::{Attribute, Listener, Node, NodeKey};
81103pub use self::render::{Render, RootRender};
104+pub use self::render_context::RenderContext;
82105pub use self::vdom::{Vdom, VdomWeak};
106+
107+cfg_if::cfg_if! {
108+ if #[cfg(all(target_arch = "wasm32", not(feature = "xxx-unstable-internal-use-only")))] {
109+ use wasm_bindgen::__rt::WasmRefCell as RefCell;
110+ } else {
111+ use std::cell::RefCell;
112+ }
113+}
114+
115+// Polyfill some Web stuff for benchmarking...
116+cfg_if::cfg_if! {
117+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
118+ /// An element node in the physical DOM.
119+ pub type Element = ();
120+
121+ pub(crate) type EventsTrampoline = ();
122+ } else {
123+ /// An element node in the physical DOM.
124+ pub type Element = web_sys::Element;
125+
126+ pub(crate) type EventsTrampoline = wasm_bindgen::closure::Closure<dyn Fn(web_sys::Event, u32, u32)>;
127+ }
128+}
129+
130+cfg_if::cfg_if! {
131+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
132+ pub use self::cached_set::{CachedSet};
133+ pub use self::node::{ElementNode, NodeKind, TextNode};
134+ }
135+}
src/node.rs+150−77
@@ -1,33 +1,111 @@
1−use crate::{RootRender, VdomWeak};
1+use crate::{cached_set::CacheId, RootRender, VdomWeak};
22use bumpalo::Bump;
33use std::fmt;
4+use std::iter;
45use std::mem;
6+use std::u32;
57
6−/// A node is either a text node or an element.
8+/// A virtual DOM node.
79#[derive(Debug, Clone)]
8−pub enum Node<'a> {
9− /// A text node.
10− Text(TextNode<'a>),
10+pub struct Node<'a> {
11+ #[cfg(feature = "xxx-unstable-internal-use-only")]
12+ #[doc(hidden)]
13+ pub kind: NodeKind<'a>,
1114
12− /// An element potentially with attributes and children.
13− Element(ElementNode<'a>),
15+ #[cfg(not(feature = "xxx-unstable-internal-use-only"))]
16+ pub(crate) kind: NodeKind<'a>,
1417}
1518
16−/// Text nodes are just a string of text. They cannot have attributes or
17−/// children.
18−#[derive(Debug, Clone)]
19−pub struct TextNode<'a> {
20− pub(crate) text: &'a str,
19+pub_unstable_internal! {
20+ /// A node is either a text node or an element.
21+ #[derive(Debug, Clone)]
22+ pub(crate) enum NodeKind<'a> {
23+ /// A text node.
24+ Text(TextNode<'a>),
25+
26+ /// An element potentially with attributes and children.
27+ Element(&'a ElementNode<'a>),
28+
29+ /// A node in the vdom's `CachedSet`. This allows us to avoid
30+ /// re-rendering and re-diffing subtrees.
31+ Cached(CachedNode),
32+ }
2133}
2234
23−/// Elements have a tag name, zero or more attributes, and zero or more
24−/// children.
25−#[derive(Debug, Clone)]
26−pub struct ElementNode<'a> {
27− pub(crate) tag_name: &'a str,
28− pub(crate) listeners: &'a [Listener<'a>],
29− pub(crate) attributes: &'a [Attribute<'a>],
30− pub(crate) children: &'a [Node<'a>],
35+pub_unstable_internal! {
36+ /// Text nodes are just a string of text. They cannot have attributes or
37+ /// children.
38+ #[derive(Debug, Clone)]
39+ pub(crate) struct TextNode<'a> {
40+ pub text: &'a str,
41+ }
42+}
43+
44+pub_unstable_internal! {
45+ /// Elements have a tag name, zero or more attributes, and zero or more
46+ /// children.
47+ #[derive(Debug, Clone)]
48+ pub(crate) struct ElementNode<'a> {
49+ pub key: NodeKey,
50+ pub tag_name: &'a str,
51+ pub listeners: &'a [Listener<'a>],
52+ pub attributes: &'a [Attribute<'a>],
53+ pub children: &'a [Node<'a>],
54+ pub namespace: Option<&'a str>,
55+ }
56+}
57+
58+pub_unstable_internal! {
59+ /// A cached node exists in an arena that is internal to the `Vdom`. It
60+ /// allows us to avoid both re-rendering a sub-tree and re-diffing
61+ /// it. `CachedNode`s cannot contain cycles, since they can only contain
62+ /// other `CachedNode`s that already exist when they are being constructed.
63+ #[derive(Debug, Clone, Copy)]
64+ pub(crate) struct CachedNode {
65+ pub id: CacheId,
66+ // Always a copy of the inner cached node's key.
67+ pub key: NodeKey,
68+ }
69+}
70+
71+/// The key for keyed children.
72+///
73+/// Keys must be unique among siblings.
74+///
75+/// If any sibling is keyed, then they all must be keyed.
76+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77+pub struct NodeKey(pub(crate) u32);
78+
79+impl Default for NodeKey {
80+ fn default() -> NodeKey {
81+ NodeKey::NONE
82+ }
83+}
84+
85+impl NodeKey {
86+ /// The default, lack of a key.
87+ pub const NONE: NodeKey = NodeKey(u32::MAX);
88+
89+ /// Is this key `NodeKey::NONE`?
90+ #[inline]
91+ pub fn is_none(&self) -> bool {
92+ *self == Self::NONE
93+ }
94+
95+ /// Is this key not `NodeKey::NONE`?
96+ #[inline]
97+ pub fn is_some(&self) -> bool {
98+ !self.is_none()
99+ }
100+
101+ /// Create a new `NodeKey`.
102+ ///
103+ /// `key` must not be `u32::MAX`.
104+ #[inline]
105+ pub fn new(key: u32) -> Self {
106+ debug_assert_ne!(key, u32::MAX);
107+ NodeKey(key)
108+ }
31109}
32110
33111/// An event listener callback function.
@@ -36,8 +114,8 @@ pub struct ElementNode<'a> {
36114///
37115/// 1. The virtual DOM's root rendering component.
38116/// 2. A capability to scheduler virtual DOM re-rendering.
39−/// 3. The event that ocurred.
40−pub type ListenerCallback<'a> =
117+/// 3. The event that occurred.
118+pub(crate) type ListenerCallback<'a> =
41119 &'a (dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static);
42120
43121/// An event listener.
@@ -56,6 +134,15 @@ pub struct Attribute<'a> {
56134 pub(crate) value: &'a str,
57135}
58136
137+impl<'a> From<CachedNode> for Node<'a> {
138+ #[inline]
139+ fn from(c: CachedNode) -> Self {
140+ Node {
141+ kind: NodeKind::Cached(c),
142+ }
143+ }
144+}
145+
59146impl fmt::Debug for Listener<'_> {
60147 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61148 let (a, b) = self.get_callback_parts();
@@ -95,81 +182,67 @@ impl<'a> Attribute<'a> {
95182}
96183
97184impl<'a> Node<'a> {
98− /// Construct a new element node with the given tag name and children.
185+ /// Low-level constructor for making a new `Node` of type element with given
186+ /// parts.
187+ ///
188+ /// This is primarily intended for JSX and templating proc-macros to compile
189+ /// down into. If you are building nodes by-hand, prefer using the
190+ /// `dodrio::builder::*` APIs.
99191 #[inline]
100− pub(crate) fn element<Listeners, Attributes, Children>(
192+ pub fn element(
101193 bump: &'a Bump,
194+ key: NodeKey,
102195 tag_name: &'a str,
103− listeners: Listeners,
104− attributes: Attributes,
105− children: Children,
106− ) -> Node<'a>
107− where
108− Listeners: 'a + AsRef<[Listener<'a>]>,
109− Attributes: 'a + AsRef<[Attribute<'a>]>,
110− Children: 'a + AsRef<[Node<'a>]>,
111− {
112− let children: &'a Children = bump.alloc(children);
113− let children: &'a [Node<'a>] = children.as_ref();
114−
115− let listeners: &'a Listeners = bump.alloc(listeners);
116− let listeners: &'a [Listener<'a>] = listeners.as_ref();
117−
118− let attributes: &'a Attributes = bump.alloc(attributes);
119− let attributes: &'a [Attribute<'a>] = attributes.as_ref();
120−
121− Node::Element(ElementNode {
196+ listeners: &'a [Listener<'a>],
197+ attributes: &'a [Attribute<'a>],
198+ children: &'a [Node<'a>],
199+ namespace: Option<&'a str>,
200+ ) -> Node<'a> {
201+ let element = bump.alloc_with(|| ElementNode {
202+ key,
122203 tag_name,
123204 listeners,
124205 attributes,
125206 children,
126− })
127− }
128−
129− /// Is this node a text node?
130− pub fn is_text(&self) -> bool {
131− match self {
132− Node::Text(_) => true,
133− _ => false,
134− }
135− }
207+ namespace,
208+ });
136209
137− /// Is this node an element?
138− pub fn is_element(&self) -> bool {
139− match self {
140− Node::Element { .. } => true,
141− _ => false,
210+ Node {
211+ kind: NodeKind::Element(element),
142212 }
143213 }
144214
145215 /// Construct a new text node with the given text.
146216 #[inline]
147217 pub(crate) fn text(text: &'a str) -> Node<'a> {
148− Node::Text(TextNode { text })
218+ Node {
219+ kind: NodeKind::Text(TextNode { text }),
220+ }
149221 }
150−}
151222
152−impl<'a> TextNode<'a> {
153− /// Get this text node's text content.
154− pub fn text(&self) -> &'a str {
155− self.text
223+ #[inline]
224+ pub(crate) fn key(&self) -> NodeKey {
225+ match &self.kind {
226+ NodeKind::Text(_) => NodeKey::NONE,
227+ NodeKind::Element(e) => e.key,
228+ NodeKind::Cached(c) => c.key,
229+ }
156230 }
157231}
158232
159−impl<'a> ElementNode<'a> {
160− /// Get this element's tag name.
161− pub fn tag_name(&self) -> &'a str {
162− self.tag_name
163− }
164−
165− /// Get this element's attributes.
166− pub fn attributes(&self) -> &'a [Attribute<'a>] {
167− self.attributes
168− }
233+/// A node can become an iterator that yields the node itself once.
234+///
235+/// This implementation of `IntoIterator` mostly exists to improve the
236+/// `typed-html` ergonomics, where the macro invokes `.into_iter()` on the child
237+/// contents of a tag. By implementing `IntoIterator` here, we avoid having to
238+/// do nasty shenanigans like `<p>vec![$contents]</p>` instead of plain old
239+/// `<p>$contents</p>`.
240+impl<'a> IntoIterator for Node<'a> {
241+ type Item = Node<'a>;
242+ type IntoIter = iter::Once<Self::Item>;
169243
170− /// Get this element's attributes.
171− pub fn children(&self) -> &'a [Node<'a>] {
172− self.children
244+ fn into_iter(self) -> Self::IntoIter {
245+ iter::once(self)
173246 }
174247}
175248
src/render.rs+65−50
@@ -1,5 +1,4 @@
1−use crate::Node;
2−use bumpalo::Bump;
1+use crate::{Node, RenderContext};
32use std::any::Any;
43use std::rc::Rc;
54use wasm_bindgen::UnwrapThrowExt;
@@ -11,72 +10,52 @@ use wasm_bindgen::UnwrapThrowExt;
1110///
1211/// ## `Bump` Allocation
1312///
14−/// `Render` implementations can use the provided `Bump` for very fast
15−/// allocation for anything that needs to be allocated during rendering.
16−///
17−/// ## The `'a: 'bump` Lifetime Bound
18−///
19−/// The `'a: 'bump` bounds enforce that `self` outlives the given bump
20−/// allocator. This means that if `self` contains a string, the string does not
21−/// need to be copied into the output `Node` and can be used by reference
22−/// instead (i.e. it prevents accidentally using the string after its been
23−/// freed). The `'a: 'bump` bound also enables abstractions like
24−/// `dodrio::Cached` that can re-use cached `Node`s across `render`s without
25−/// copying them.
13+/// `Render` implementations can use the `Bump` inside the provided
14+/// `RenderContext` for very fast allocation for anything that needs to be
15+/// temporarily allocated during rendering.
2616///
2717/// ## Example
2818///
2919/// ```no_run
30−/// use dodrio::{bumpalo::Bump, Node, Render};
20+/// use dodrio::{Node, Render, RenderContext};
3121///
3222/// pub struct MyComponent;
3323///
34−/// impl Render for MyComponent {
35−/// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
36−/// where
37−/// 'a: 'bump
38−/// {
24+/// impl<'a> Render<'a> for MyComponent {
25+/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3926/// use dodrio::builder::*;
4027///
41−/// p(bump)
28+/// p(&cx)
4229/// .children([
4330/// text("This is "),
44−/// strong(bump).children([text("my component")]).finish(),
31+/// strong(&cx).children([text("my component")]).finish(),
4532/// text(" rendered!"),
4633/// ])
4734/// .finish()
4835/// }
4936/// }
5037/// ```
51−pub trait Render {
52− /// Render `self` as a virtual DOM. Use the given `Bump` for temporary
53− /// allocations.
54− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
55− where
56− 'a: 'bump;
38+pub trait Render<'a> {
39+ /// Render `self` as a virtual DOM. Use the given context's `Bump` for
40+ /// temporary allocations.
41+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a>;
5742}
5843
59−impl<'r, R> Render for &'r R
44+impl<'a, 'r, R> Render<'a> for &'r R
6045where
61− R: Render,
46+ R: Render<'a>,
6247{
63− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
64− where
65− 'a: 'bump,
66− {
67− (**self).render(bump)
48+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
49+ (**self).render(cx)
6850 }
6951}
7052
71−impl<R> Render for Rc<R>
53+impl<'a, R> Render<'a> for Rc<R>
7254where
73− R: Render,
55+ R: Render<'a>,
7456{
75− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
76− where
77− 'a: 'bump,
78− {
79− (**self).render(bump)
57+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
58+ (**self).render(cx)
8059 }
8160}
8261
@@ -90,21 +69,24 @@ where
9069/// You do not need to implement this trait by hand: there is a blanket
9170/// implementation for all `Render` types that fulfill the `RootRender`
9271/// requirements.
93−pub trait RootRender: Any + Render {
72+pub trait RootRender: Any + for<'a> Render<'a> {
9473 /// Get this `&RootRender` trait object as an `&Any` trait object reference.
95− fn as_any(&self) -> &Any;
74+ fn as_any(&self) -> &dyn Any;
9675
9776 /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
9877 /// reference.
99− fn as_any_mut(&mut self) -> &mut Any;
78+ fn as_any_mut(&mut self) -> &mut dyn Any;
10079}
10180
102−impl<T: Any + Render> RootRender for T {
103− fn as_any(&self) -> &Any {
81+impl<T> RootRender for T
82+where
83+ T: Any + for<'a> Render<'a>,
84+{
85+ fn as_any(&self) -> &dyn Any {
10486 self
10587 }
10688
107− fn as_any_mut(&mut self) -> &mut Any {
89+ fn as_any_mut(&mut self) -> &mut dyn Any {
10890 self
10991 }
11092}
@@ -117,7 +99,10 @@ impl dyn RootRender {
11799 ///
118100 /// Panics if this virtual DOM's root rendering component is not an `R`
119101 /// instance.
120− pub fn unwrap_ref<R: RootRender>(&self) -> &R {
102+ pub fn unwrap_ref<R>(&self) -> &R
103+ where
104+ R: RootRender,
105+ {
121106 self.as_any()
122107 .downcast_ref::<R>()
123108 .expect_throw("bad `RootRender::unwrap_ref` call")
@@ -130,7 +115,10 @@ impl dyn RootRender {
130115 ///
131116 /// Panics if this virtual DOM's root rendering component is not an `R`
132117 /// instance.
133− pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
118+ pub fn unwrap_mut<R>(&mut self) -> &mut R
119+ where
120+ R: RootRender,
121+ {
134122 self.as_any_mut()
135123 .downcast_mut::<R>()
136124 .expect_throw("bad `RootRender::unwrap_ref` call")
@@ -150,4 +138,31 @@ mod tests {
150138 #[allow(dead_code)]
151139 fn takes_dyn_render(_: &dyn super::RootRender) {}
152140 }
141+
142+ #[test]
143+ fn render_bump_scoped_child() {
144+ use crate::{builder::*, bumpalo::collections::String, Node, Render, RenderContext};
145+
146+ struct Child<'a> {
147+ name: &'a str,
148+ }
149+
150+ impl<'a> Render<'a> for Child<'a> {
151+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
152+ text(self.name)
153+ }
154+ }
155+
156+ struct Parent;
157+
158+ impl<'a> Render<'a> for Parent {
159+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
160+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
161+
162+ div(&cx)
163+ .children([Child { name: child_name }.render(cx)])
164+ .finish()
165+ }
166+ }
167+ }
153168}
src/render_context.rsnew file+104
@@ -0,0 +1,104 @@
1+use crate::{
2+ cached::{Cached, TemplateId},
3+ cached_set::{CacheId, CachedSet},
4+ Node, Render,
5+};
6+use bumpalo::Bump;
7+use fxhash::FxHashMap;
8+use std::fmt;
9+
10+/// Common context available to all `Render` implementations.
11+///
12+/// Notably, the `RenderContext` gives access to the bump arena that the virtual
13+/// DOM should be allocated within. This is available via the `bump` field.
14+pub struct RenderContext<'a> {
15+ /// The underlying bump arena that virtual DOMs are rendered into.
16+ ///
17+ /// ## Example
18+ ///
19+ /// ```
20+ /// use dodrio::RenderContext;
21+ ///
22+ /// // Given a rendering context, allocate an i32 inside its bump arena.
23+ /// fn foo<'a>(cx: &mut RenderContext<'a>) -> &'a mut i32 {
24+ /// cx.bump.alloc(42)
25+ /// }
26+ /// ```
27+ pub bump: &'a Bump,
28+
29+ pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
30+
31+ pub(crate) templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>,
32+
33+ // Prevent exhaustive matching on the rendering context, so we can always
34+ // add more members in a semver-compatible way.
35+ _non_exhaustive: (),
36+}
37+
38+impl fmt::Debug for RenderContext<'_> {
39+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40+ f.debug_struct("RenderContext")
41+ .field("bump", &self.bump)
42+ .finish()
43+ }
44+}
45+
46+impl<'a> RenderContext<'a> {
47+ pub_unstable_internal! {
48+ pub(crate) fn new(
49+ bump: &'a Bump,
50+ cached_set: &'a crate::RefCell<CachedSet>,
51+ templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>
52+ ) -> Self {
53+ RenderContext {
54+ bump,
55+ cached_set,
56+ templates,
57+ _non_exhaustive: (),
58+ }
59+ }
60+ }
61+
62+ pub(crate) fn cache<F>(&mut self, pinned: bool, template: Option<CacheId>, f: F) -> CacheId
63+ where
64+ F: for<'b> FnOnce(&mut RenderContext<'b>) -> Node<'b>,
65+ {
66+ CachedSet::insert(self, pinned, template, f)
67+ }
68+
69+ /// Get or create the cached template for `Cached<R>`.
70+ pub(crate) fn template<R>(&mut self) -> Option<CacheId>
71+ where
72+ R: 'static + Default + for<'b> Render<'b>,
73+ {
74+ let template_id = Cached::<R>::template_id();
75+ if let Some(cache_id) = self.templates.get(&template_id).cloned() {
76+ return cache_id;
77+ }
78+
79+ // Prevent re-entrancy from infinite looping. Any attempts to get `R`'s
80+ // template while constructing the template will simply fail to use the
81+ // templated fast path.
82+ self.templates.insert(template_id, None);
83+
84+ // Render the default `R` and save that as the template for all
85+ // `Cached<R>`s.
86+ let cache_id = self.cache(true, None, |nested_cx| R::default().render(nested_cx));
87+ self.templates.insert(template_id, Some(cache_id));
88+ Some(cache_id)
89+ }
90+}
91+
92+impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
93+ #[inline]
94+ fn from(cx: &'b RenderContext<'a>) -> &'a Bump {
95+ cx.bump
96+ }
97+}
98+
99+impl<'a, 'b, 'c> From<&'c &'b mut RenderContext<'a>> for &'a Bump {
100+ #[inline]
101+ fn from(cx: &'c &'b mut RenderContext<'a>) -> &'a Bump {
102+ cx.bump
103+ }
104+}
src/strace.rsnew file+22
@@ -0,0 +1,22 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(feature = "xxx-unstable-strace")] {
3+ use wasm_bindgen::prelude::*;
4+
5+ #[wasm_bindgen(module = "/js/strace.js")]
6+ extern "C" {
7+ #[wasm_bindgen(js_name = initStrace)]
8+ fn really_init_strace();
9+ }
10+
11+ pub fn init_strace() {
12+ use std::sync::Once;
13+ static STRACE: Once = Once::new();
14+ STRACE.call_once(|| {
15+ really_init_strace();
16+ });
17+ }
18+ } else {
19+ #[inline]
20+ pub fn init_strace() {}
21+ }
22+}
src/vdom.rs+182−338
@@ -1,13 +1,16 @@
1−use super::change_list::ChangeList;
2−use super::node::{Attribute, ElementNode, Listener, Node, TextNode};
1+use super::change_list::ChangeListPersistentState;
32use super::RootRender;
3+use crate::cached::TemplateId;
4+use crate::cached_set::{CacheId, CachedSet};
45use crate::events::EventsRegistry;
6+use crate::node::{Node, NodeKey};
7+use crate::RenderContext;
58use bumpalo::Bump;
6−use futures::future::Future;
9+use fxhash::FxHashMap;
710use std::cell::Cell;
811use std::cell::RefCell;
9−use std::cmp;
1012use std::fmt;
13+use std::future::Future;
1114use std::mem;
1215use std::mem::ManuallyDrop;
1316use std::rc::{Rc, Weak};
@@ -52,12 +55,15 @@ pub(crate) struct VdomInnerExclusive {
5255 // Always `Some` except just before we drop. Just an option so that
5356 // `unmount` can take the component out but we can still have a Drop
5457 // implementation.
55− component: Option<Box<RootRender>>,
58+ component: Option<Box<dyn RootRender>>,
5659
57− dom_buffers: [Bump; 2],
58− change_list: ManuallyDrop<ChangeList>,
59− container: web_sys::Element,
60+ dom_buffers: Option<[Bump; 2]>,
61+ change_list: ManuallyDrop<ChangeListPersistentState>,
62+ container: crate::Element,
6063 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
64+ events_trampoline: Option<crate::EventsTrampoline>,
65+ cached_set: crate::RefCell<CachedSet>,
66+ templates: FxHashMap<TemplateId, Option<CacheId>>,
6167
6268 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
6369 // caching renders, into `self.component`'s bump.
@@ -90,6 +96,7 @@ impl fmt::Debug for VdomInnerExclusive {
9096 .field("change_list", &self.change_list)
9197 .field("container", &self.container)
9298 .field("events_registry", &self.events_registry)
99+ .field("events_trampoline", &"..")
93100 .field("current_root", &self.current_root)
94101 .finish()
95102 }
@@ -109,7 +116,36 @@ impl Drop for VdomInnerExclusive {
109116 let mut registry = registry.borrow_mut();
110117 registry.clear_active_listeners();
111118
112− self.container.set_inner_html("");
119+ empty_container(&self.container);
120+ }
121+}
122+
123+cfg_if::cfg_if! {
124+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
125+ fn empty_container(_container: &crate::Element) {}
126+ fn initialize_container(_container: &crate::Element) {}
127+ } else {
128+ fn empty_container(container: &crate::Element) {
129+ container.set_inner_html("");
130+ }
131+
132+ fn initialize_container(container: &crate::Element) {
133+ empty_container(container);
134+
135+ // Create the dummy `<div/>` child in the container.
136+ let window = web_sys::window().expect_throw("should have access to the Window");
137+ let document = window
138+ .document()
139+ .expect("should have access to the Document");
140+ container
141+ .append_child(
142+ document
143+ .create_element("div")
144+ .expect("should create element OK")
145+ .as_ref(),
146+ )
147+ .expect("should append child OK");
148+ }
113149 }
114150}
115151
@@ -118,40 +154,29 @@ impl Vdom {
118154 /// rendering component.
119155 ///
120156 /// This will box the given component into trait object.
121− pub fn new<R>(container: &web_sys::Element, component: R) -> Vdom
157+ pub fn new<R>(container: &crate::Element, component: R) -> Vdom
122158 where
123159 R: RootRender,
124160 {
125− Self::with_boxed_root_render(container, Box::new(component) as Box<RootRender>)
161+ Self::with_boxed_root_render(container, Box::new(component) as Box<dyn RootRender>)
126162 }
127163
128164 /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
129165 /// rendering component.
130166 pub fn with_boxed_root_render(
131− container: &web_sys::Element,
132− component: Box<RootRender>,
167+ container: &crate::Element,
168+ component: Box<dyn RootRender>,
133169 ) -> Vdom {
134− let dom_buffers = [Bump::new(), Bump::new()];
135− let change_list = ManuallyDrop::new(ChangeList::new(container));
170+ crate::strace::init_strace();
136171
137− // Ensure that the container is empty.
138− container.set_inner_html("");
172+ let dom_buffers = [Bump::new(), Bump::new()];
173+ let change_list = ManuallyDrop::new(ChangeListPersistentState::new(container));
139174
140175 // Create a dummy `<div/>` in our container.
141− let current_root = Node::element(&dom_buffers[0], "div", [], [], []);
176+ initialize_container(container);
177+ let current_root =
178+ Node::element(&dom_buffers[0], NodeKey::NONE, "div", &[], &[], &[], None);
142179 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
143− let window = web_sys::window().expect("should have acess to the Window");
144− let document = window
145− .document()
146− .expect("should have access to the Document");
147− container
148− .append_child(
149− document
150− .create_element("div")
151− .expect("should create element OK")
152− .as_ref(),
153− )
154− .expect("should append child OK");
155180
156181 let container = container.clone();
157182 let inner = Rc::new(VdomInner {
@@ -160,11 +185,14 @@ impl Vdom {
160185 },
161186 exclusive: RefCell::new(VdomInnerExclusive {
162187 component: Some(component),
163− dom_buffers,
188+ dom_buffers: Some(dom_buffers),
164189 change_list,
165190 container,
166191 current_root,
167192 events_registry: None,
193+ events_trampoline: None,
194+ cached_set: crate::RefCell::new(Default::default()),
195+ templates: Default::default(),
168196 }),
169197 });
170198
@@ -173,7 +201,9 @@ impl Vdom {
173201 {
174202 let mut inner = inner.exclusive.borrow_mut();
175203 inner.events_registry = Some(events_registry);
176− inner.change_list.init_events_trampoline(events_trampoline);
204+ inner.change_list.init_events_trampoline(&events_trampoline);
205+ debug_assert!(inner.events_trampoline.is_none());
206+ inner.events_trampoline = Some(events_trampoline);
177207
178208 // Diff and apply the `contents` against our dummy `<div/>`.
179209 inner.render();
@@ -182,6 +212,19 @@ impl Vdom {
182212 Vdom { inner }
183213 }
184214
215+ /// Immediately re-render and diff. Only for internal testing and
216+ /// benchmarking purposes.
217+ #[cfg(feature = "xxx-unstable-internal-use-only")]
218+ pub fn immediately_render_and_diff<R>(&self, component: R)
219+ where
220+ R: RootRender,
221+ {
222+ let mut exclusive = self.inner.exclusive.borrow_mut();
223+ let component = Box::new(component) as Box<dyn RootRender>;
224+ exclusive.component = Some(component);
225+ exclusive.render();
226+ }
227+
185228 /// Run this virtual DOM and its listeners forever and never unmount it.
186229 #[inline]
187230 pub fn forget(self) {
@@ -197,7 +240,7 @@ impl Vdom {
197240 /// Unmount this virtual DOM, unregister its event listeners, and return its
198241 /// root render component.
199242 #[inline]
200− pub fn unmount(self) -> Box<RootRender> {
243+ pub fn unmount(self) -> Box<dyn RootRender> {
201244 Rc::try_unwrap(self.inner.clone())
202245 .map_err(|_| ())
203246 .unwrap_throw()
@@ -221,268 +264,69 @@ impl VdomInnerExclusive {
221264 pub(crate) fn render(&mut self) {
222265 unsafe {
223266 let events_registry = self.events_registry.take().unwrap();
224−
225267 {
226− // All the old listeners are no longer active. We will build a new
227− // set of active listeners when diffing.
228− //
229− // NB: if we end up avoiding diffing cached renders (instead of just
230− // avoiding re-rendering them) then we will need to maintain cached
231− // active listeners, and can't just clear all active listeners and
232− // rebuild them here.
233268 let mut registry = events_registry.borrow_mut();
234− registry.clear_active_listeners();
235269
236270 // Reset the inactive bump arena's pointer.
237− self.dom_buffers[1].reset();
271+ let mut dom_buffers = self.dom_buffers.take().unwrap_throw();
272+ dom_buffers[1].reset();
238273
239274 // Render the new current contents into the inactive bump arena.
240− let new_contents = self
241− .component
242− .as_ref()
243− .unwrap_throw()
244− .render(&self.dom_buffers[1]);
275+ let mut cx =
276+ RenderContext::new(&dom_buffers[1], &self.cached_set, &mut self.templates);
277+ let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
245278 let new_contents = extend_node_lifetime(new_contents);
246279
247280 // Diff the old contents with the new contents.
248281 let old_contents = self.current_root.take().unwrap();
249− self.diff(&mut registry, old_contents, new_contents.clone());
282+ let mut cache_roots;
283+ {
284+ let cached_set = self.cached_set.borrow();
285+ cache_roots = cached_set.new_roots_set();
286+ let mut change_list = self.change_list.builder();
287+ crate::diff::diff(
288+ &cached_set,
289+ &mut change_list,
290+ &mut registry,
291+ &old_contents,
292+ &new_contents,
293+ &mut cache_roots,
294+ );
295+
296+ // Tell JS to apply our diff-generated changes to the physical DOM!
297+ change_list.finish();
298+ }
299+
300+ {
301+ // Clean up unused cached renders.
302+ let mut cached_set = self.cached_set.borrow_mut();
303+ cached_set.gc(&mut registry, cache_roots);
304+ }
250305
251306 // Swap the buffers to make the bump arena with the new contents the
252307 // active arena, and the old one into the inactive arena.
253− self.swap_buffers();
308+ self.swap_buffers(dom_buffers);
254309 self.set_current_root(new_contents);
255310 }
256311
257312 self.events_registry = Some(events_registry);
258−
259− // Find and drop cached strings that aren't in use anymore.
260− self.change_list.drop_unused_strings();
261−
262− // Tell JS to apply our diff-generated changes to the physical DOM!
263− self.change_list.apply_changes();
264313 }
265314 }
266315
267− fn swap_buffers(&mut self) {
268− let (first, second) = self.dom_buffers.as_mut().split_at_mut(1);
316+ fn swap_buffers(&mut self, mut dom_buffers: [Bump; 2]) {
317+ debug_assert!(self.dom_buffers.is_none());
318+ let (first, second) = dom_buffers.as_mut().split_at_mut(1);
269319 mem::swap(&mut first[0], &mut second[0]);
320+ self.dom_buffers = Some(dom_buffers);
270321 }
271322
272323 unsafe fn set_current_root(&mut self, current: Node<'static>) {
273324 debug_assert!(self.current_root.is_none());
274325 self.current_root = Some(current);
275326 }
276−
277− fn diff<'a>(&mut self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
278− // debug!("---------------------------------------------------------");
279− // debug!("dodrio::Vdom::diff");
280− // debug!(" old = {:#?}", old);
281− // debug!(" new = {:#?}", new);
282− match (&new, old) {
283− (&Node::Text(TextNode { text: new_text }), Node::Text(TextNode { text: old_text })) => {
284− debug!(" both are text nodes");
285− if new_text != old_text {
286− debug!(" text needs updating");
287− self.change_list.emit_set_text(new_text);
288− }
289− }
290− (&Node::Text(TextNode { .. }), Node::Element(ElementNode { .. })) => {
291− debug!(" replacing a text node with an element");
292− self.create(registry, new);
293− self.change_list.emit_replace_with();
294− }
295− (&Node::Element(ElementNode { .. }), Node::Text(TextNode { .. })) => {
296− debug!(" replacing an element with a text node");
297− self.create(registry, new);
298− self.change_list.emit_replace_with();
299− }
300− (
301− &Node::Element(ElementNode {
302− tag_name: new_tag_name,
303− listeners: new_listeners,
304− attributes: new_attributes,
305− children: new_children,
306− }),
307− Node::Element(ElementNode {
308− tag_name: old_tag_name,
309− listeners: old_listeners,
310− attributes: old_attributes,
311− children: old_children,
312− }),
313− ) => {
314− debug!(" updating an element");
315− if new_tag_name != old_tag_name {
316− debug!(" different tag names; creating new element and replacing old element");
317− self.create(registry, new);
318− self.change_list.emit_replace_with();
319− return;
320− }
321− self.diff_listeners(registry, old_listeners, new_listeners);
322− self.diff_attributes(old_attributes, new_attributes);
323− self.diff_children(registry, old_children, new_children);
324− }
325− }
326− }
327−
328− fn diff_listeners<'a>(
329− &mut self,
330− registry: &mut EventsRegistry,
331− old: &'a [Listener<'a>],
332− new: &'a [Listener<'a>],
333− ) {
334− debug!(" updating event listeners");
335−
336− 'outer1: for new_l in new {
337− unsafe {
338− // Safety relies on removing `new_l` from the registry manually
339− // at the end of its lifetime. This happens when we invoke
340− // `clear_active_listeners` at the start of a new rendering
341− // phase.
342− registry.add(new_l);
343− }
344− for old_l in old {
345− if new_l.event == old_l.event {
346− self.change_list.emit_update_event_listener(new_l);
347− continue 'outer1;
348− }
349− }
350− self.change_list.emit_new_event_listener(new_l);
351− }
352−
353− 'outer2: for old_l in old {
354− for new_l in new {
355− if new_l.event == old_l.event {
356− continue 'outer2;
357− }
358− }
359− self.change_list.emit_remove_event_listener(old_l.event);
360− }
361− }
362−
363− fn diff_attributes(&mut self, old: &[Attribute], new: &[Attribute]) {
364− debug!(" updating attributes");
365−
366− // Do O(n^2) passes to add/update and remove attributes, since
367− // there are almost always very few attributes.
368− 'outer: for new_attr in new {
369− if new_attr.is_volatile() {
370− self.change_list
371− .emit_set_attribute(new_attr.name, new_attr.value);
372− } else {
373− for old_attr in old {
374− if old_attr.name == new_attr.name {
375− if old_attr.value != new_attr.value {
376− self.change_list
377− .emit_set_attribute(new_attr.name, new_attr.value);
378− }
379− continue 'outer;
380− }
381− }
382− self.change_list
383− .emit_set_attribute(new_attr.name, new_attr.value);
384− }
385− }
386−
387− 'outer2: for old_attr in old {
388− for new_attr in new {
389− if old_attr.name == new_attr.name {
390− continue 'outer2;
391− }
392− }
393− self.change_list.emit_remove_attribute(old_attr.name);
394− }
395− }
396−
397− fn diff_children<'a>(
398− &mut self,
399− registry: &mut EventsRegistry,
400− old: &'a [Node<'a>],
401− new: &'a [Node<'a>],
402− ) {
403− debug!(" updating children shared by old and new");
404−
405− let num_children_to_diff = cmp::min(new.len(), old.len());
406− let mut new_children = new.iter();
407− let mut old_children = old.iter();
408− let mut pushed = false;
409−
410− for (i, (new_child, old_child)) in new_children
411− .by_ref()
412− .zip(old_children.by_ref())
413− .take(num_children_to_diff)
414− .enumerate()
415− {
416− if i == 0 {
417− self.change_list.emit_push_first_child();
418− pushed = true;
419− } else {
420− debug_assert!(pushed);
421− self.change_list.emit_pop_push_next_sibling();
422− }
423−
424− self.diff(registry, old_child.clone(), new_child.clone());
425− }
426−
427− if old_children.next().is_some() {
428− debug!(" removing extra old children");
429− debug_assert!(new_children.next().is_none());
430− if !pushed {
431− self.change_list.emit_push_first_child();
432− } else {
433− self.change_list.emit_pop_push_next_sibling();
434− }
435− self.change_list.emit_remove_self_and_next_siblings();
436− pushed = false;
437− } else {
438− debug!(" creating new children");
439− for (i, new_child) in new_children.enumerate() {
440− if i == 0 && pushed {
441− self.change_list.emit_pop();
442− pushed = false;
443− }
444− self.create(registry, new_child.clone());
445− self.change_list.emit_append_child();
446− }
447− }
448−
449− debug!(" done updating children");
450− if pushed {
451− self.change_list.emit_pop();
452− }
453− }
454−
455− fn create<'a>(&mut self, registry: &mut EventsRegistry, node: Node<'a>) {
456− match node {
457− Node::Text(TextNode { text }) => {
458− self.change_list.emit_create_text_node(text);
459− }
460− Node::Element(ElementNode {
461− tag_name,
462− listeners,
463− attributes,
464− children,
465− }) => {
466− self.change_list.emit_create_element(tag_name);
467− for l in listeners {
468− unsafe {
469− registry.add(l);
470− }
471− self.change_list.emit_new_event_listener(l);
472− }
473− for attr in attributes {
474− self.change_list.emit_set_attribute(&attr.name, &attr.value);
475− }
476− for child in children {
477− self.create(registry, child.clone());
478− self.change_list.emit_append_child();
479− }
480− }
481− }
482− }
483327}
484328
485−fn request_animation_frame(f: &Closure<FnMut()>) {
329+fn request_animation_frame(f: &Closure<dyn FnMut()>) {
486330 web_sys::window()
487331 .expect_throw("should have a window")
488332 .request_animation_frame(f.as_ref().unchecked_ref())
@@ -499,7 +343,7 @@ where
499343 let f = Closure::wrap(Box::new(move || {
500344 *g.borrow_mut() = None;
501345 f();
502− }) as Box<FnMut()>);
346+ }) as Box<dyn FnMut()>);
503347 request_animation_frame(&f);
504348
505349 *h.borrow_mut() = Some(f);
@@ -530,25 +374,23 @@ impl VdomWeak {
530374 /// Replace the root rendering component with the new `root`.
531375 ///
532376 /// Returns a future that resolves to the *old* root component.
533− pub fn set_component(
377+ pub async fn set_component(
534378 self,
535379 root: Box<dyn RootRender>,
536− ) -> impl Future<Item = Box<dyn RootRender + 'static>, Error = VdomDroppedError> {
537− futures::future::ok(self.inner.upgrade())
538− .and_then(|inner| inner.ok_or(()))
539− .map_err(|_| VdomDroppedError {})
540− .and_then(|inner| {
541− let promise = js_sys::Promise::resolve(&JsValue::null());
542− JsFuture::from(promise)
543− .map_err(|_| VdomDroppedError {})
544− .and_then(move |_| {
545− let old = {
546− let mut exclusive = inner.exclusive.borrow_mut();
547− mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
548− };
549− VdomWeak::new(&inner).render().map(|_| old)
550− })
551− })
380+ ) -> Result<Box<dyn RootRender + 'static>, VdomDroppedError> {
381+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
382+
383+ // Wait for a new tick of the micro-task queue
384+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
385+
386+ let old = {
387+ let mut exclusive = inner.exclusive.borrow_mut();
388+ mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
389+ };
390+
391+ VdomWeak::new(&inner).render().await?;
392+
393+ Ok(old)
552394 }
553395
554396 /// Execute `f` with a reference to this virtual DOM's root rendering
@@ -556,24 +398,18 @@ impl VdomWeak {
556398 ///
557399 /// To ensure exclusive access to the root rendering component, the
558400 /// invocation takes place on a new tick of the micro-task queue.
559− pub fn with_component<F, T>(&self, f: F) -> impl Future<Item = T, Error = VdomDroppedError>
401+ pub async fn with_component<F, T>(&self, f: F) -> Result<T, VdomDroppedError>
560402 where
561403 F: 'static + FnOnce(&mut dyn RootRender) -> T,
562404 {
563− futures::future::ok(self.inner.upgrade())
564− .and_then(|inner| inner.ok_or(()))
565− .map_err(|_| VdomDroppedError {})
566− .and_then(|inner| {
567− let mut f = Some(f);
568− let promise = js_sys::Promise::resolve(&JsValue::null());
569− JsFuture::from(promise)
570− .map_err(|_| VdomDroppedError {})
571− .map(move |_| {
572− let f = f.take().unwrap_throw();
573− let mut exclusive = inner.exclusive.borrow_mut();
574− f(exclusive.component_raw_mut())
575− })
576− })
405+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
406+
407+ // Wait for a new tick of the micro-task queue
408+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
409+
410+ let mut exclusive = inner.exclusive.borrow_mut();
411+
412+ Ok(f(exclusive.component_raw_mut()))
577413 }
578414
579415 /// Schedule a render to occur during the next animation frame.
@@ -582,7 +418,12 @@ impl VdomWeak {
582418 /// `render` instead.
583419 pub fn schedule_render(&self) {
584420 debug!("VdomWeak::schedule_render");
585− wasm_bindgen_futures::spawn_local(self.render().map_err(|_| ()));
421+
422+ let future = self.render();
423+
424+ wasm_bindgen_futures::spawn_local(async move {
425+ let _ = future.await;
426+ });
586427 }
587428
588429 /// Schedule a render to occur during the next animation frame and return a
@@ -590,43 +431,46 @@ impl VdomWeak {
590431 ///
591432 /// If you don't want to do more things after the render completes, then use
592433 /// `schedule_render` instead of `render`.
593− pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
594− debug!("VdomWeak::render: initiating render in new animation frame");
595− futures::future::ok(self.inner.upgrade())
596− .and_then(|inner| inner.ok_or(()))
597− .map_err(|_| VdomDroppedError {})
598− .and_then(|inner| {
599− let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
600− js_sys::Promise::new(&mut |resolve, reject| {
601− let vdom = VdomWeak {
602− inner: Rc::downgrade(&inner),
603− };
604− with_animation_frame(move || match vdom.inner.upgrade() {
605− None => {
606− warn!("VdomWeak::render: vdom unmounted before we could render");
607− let r = reject.call0(&JsValue::null());
608− debug_assert!(r.is_ok());
609− }
610− Some(inner) => {
611− let mut exclusive = inner.exclusive.borrow_mut();
612− exclusive.render();
613−
614− // We did the render, so take the promise away
615− // and let future `render` calls request new
616− // animation frames.
617− let _ = inner.shared.render_scheduled.take();
618−
619− debug!("VdomWeak::render: finished rendering");
620− let r = resolve.call0(&JsValue::null());
621− debug_assert!(r.is_ok());
622− }
623− });
624− })
625− });
626− inner.shared.render_scheduled.set(Some(promise.clone()));
627− JsFuture::from(promise)
628− .map(|_| ())
629− .map_err(|_| VdomDroppedError {})
630− })
434+ pub fn render(&self) -> impl Future<Output = Result<(), VdomDroppedError>> {
435+ let inner = self.inner.upgrade();
436+
437+ async move {
438+ let inner = inner.ok_or(VdomDroppedError {})?;
439+
440+ let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
441+ js_sys::Promise::new(&mut |resolve, reject| {
442+ let vdom = VdomWeak {
443+ inner: Rc::downgrade(&inner),
444+ };
445+
446+ with_animation_frame(move || match vdom.inner.upgrade() {
447+ None => {
448+ warn!("VdomWeak::render: vdom unmounted before we could render");
449+ let r = reject.call0(&JsValue::null());
450+ debug_assert!(r.is_ok());
451+ }
452+ Some(inner) => {
453+ let mut exclusive = inner.exclusive.borrow_mut();
454+ exclusive.render();
455+
456+ // We did the render, so take the promise away
457+ // and let future `render` calls request new
458+ // animation frames.
459+ let _ = inner.shared.render_scheduled.take();
460+
461+ let r = resolve.call0(&JsValue::null());
462+ debug_assert!(r.is_ok());
463+ }
464+ });
465+ })
466+ });
467+
468+ inner.shared.render_scheduled.set(Some(promise.clone()));
469+
470+ JsFuture::from(promise)
471+ .await
472+ .map(drop)
473+ .map_err(|_| VdomDroppedError {})
474+ }
631475 }
632476}
tests/web/cached.jsnew file+11
@@ -0,0 +1,11 @@
1+let cloneNodeCount = 0;
2+
3+const origCloneNode = Node.prototype.cloneNode;
4+Node.prototype.cloneNode = function (...args) {
5+ cloneNodeCount += 1;
6+ return origCloneNode.apply(this, args);
7+};
8+
9+export function getCloneNodeCount() {
10+ return cloneNodeCount;
11+}
tests/web/cached.rs+239−51
@@ -1,11 +1,11 @@
1−use super::{assert_rendered, create_element, RenderFn};
2−use dodrio::{builder::*, bumpalo::Bump, Cached, Node, Render, Vdom};
3−use futures::prelude::*;
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2+use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, RootRender, Vdom};
43use std::cell::Cell;
54use std::rc::Rc;
6−use wasm_bindgen::*;
5+use wasm_bindgen::prelude::*;
76use wasm_bindgen_test::*;
87
8+#[derive(Default)]
99pub struct CountRenders {
1010 render_count: Cell<usize>,
1111}
@@ -18,21 +18,18 @@ impl CountRenders {
1818 }
1919}
2020
21−impl Render for CountRenders {
22− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
23− where
24− 'a: 'bump,
25− {
21+impl<'a> Render<'a> for CountRenders {
22+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
2623 let count = self.render_count.get() + 1;
2724 self.render_count.set(count);
2825
29− let s = bumpalo::format!(in bump, "{}", count);
26+ let s = bumpalo::format!(in cx.bump, "{}", count);
3027 text(s.into_bump_str())
3128 }
3229}
3330
34−#[wasm_bindgen_test(async)]
35−fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
31+#[wasm_bindgen_test]
32+async fn uses_cached_render() {
3633 use dodrio::builder::*;
3734
3835 let cached = Cached::new(CountRenders::new());
@@ -52,50 +49,241 @@ fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
5249 let vdom6 = vdom0.clone();
5350 let vdom7 = vdom0.clone();
5451
52+ // We render, populate the cache, and get "1".
53+ vdom0.weak().render().await.unwrap();
5554 vdom0
5655 .weak()
57− // We render, populate the cache, and get "1".
58− .render()
59− .and_then(move |_| {
60− vdom0.weak().with_component(move |comp| {
61− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
62− assert_eq!(comp.render_count.get(), 1);
63− assert_rendered(&container1, &RenderFn(|_| text("1")));
64− })
56+ .with_component(move |comp| {
57+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
58+ assert_eq!(comp.render_count.get(), 1);
59+ assert_rendered(&container1, &RenderFn(|_| text("1")));
6560 })
66− // We re-render, re-use the cached node, and get "1" again.
67− .and_then(move |_| vdom1.weak().render())
68− .and_then(move |_| {
69− vdom2.weak().with_component(move |comp| {
70− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
71− assert_eq!(comp.render_count.get(), 1);
72− assert_rendered(&container2, &RenderFn(|_| text("1")));
73− })
61+ .await
62+ .unwrap();
63+
64+ // We re-render, re-use the cached node, and get "1" again.
65+ vdom1.weak().render().await.unwrap();
66+ vdom2
67+ .weak()
68+ .with_component(move |comp| {
69+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
70+ assert_eq!(comp.render_count.get(), 1);
71+ assert_rendered(&container2, &RenderFn(|_| text("1")));
7472 })
75− // We invalidate the cache, re-render, re-populate the cache, and should
76− // now have "2".
77− .and_then(move |_| {
78− vdom3.weak().with_component(move |comp| {
79− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
80− Cached::invalidate(comp);
81− })
73+ .await
74+ .unwrap();
75+
76+ // We invalidate the cache, re-render, re-populate the cache, and should now have "2".
77+ vdom3
78+ .weak()
79+ .with_component(move |comp| {
80+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
81+ Cached::invalidate(comp);
8282 })
83− .and_then(move |_| vdom4.weak().render())
84− .and_then(move |_| {
85− vdom5.weak().with_component(move |comp| {
86− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
87− assert_eq!(comp.render_count.get(), 2);
88− assert_rendered(&container3, &RenderFn(|_| text("2")));
89− })
83+ .await
84+ .unwrap();
85+ vdom4.weak().render().await.unwrap();
86+ vdom5
87+ .weak()
88+ .with_component(move |comp| {
89+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
90+ assert_eq!(comp.render_count.get(), 2);
91+ assert_rendered(&container3, &RenderFn(|_| text("2")));
9092 })
91− // We re-render, re-use the cached node, and get "2" again.
92− .and_then(move |_| vdom6.weak().render())
93− .and_then(move |_| {
94− vdom7.weak().with_component(move |comp| {
95− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
96− assert_eq!(comp.render_count.get(), 2);
97− assert_rendered(&container4, &RenderFn(|_| text("2")));
98− })
93+ .await
94+ .unwrap();
95+
96+ // We re-render, re-use the cached node, and get "2" again.
97+ vdom6.weak().render().await.unwrap();
98+ vdom7
99+ .weak()
100+ .with_component(move |comp| {
101+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
102+ assert_eq!(comp.render_count.get(), 2);
103+ assert_rendered(&container4, &RenderFn(|_| text("2")));
99104 })
100− .map_err(|e| JsValue::from(e.to_string()))
105+ .await
106+ .unwrap();
107+}
108+
109+#[wasm_bindgen(module = "/tests/web/cached.js")]
110+extern "C" {
111+ #[wasm_bindgen(js_name = getCloneNodeCount)]
112+ fn get_clone_node_count() -> u32;
113+}
114+
115+#[wasm_bindgen_test]
116+async fn creating_cached_nodes_uses_clone_node() {
117+ use dodrio::builder::*;
118+
119+ let container = create_element("div");
120+
121+ // The initial vdom render should populate the cache and save a template.
122+ let vdom0 = Rc::new(Vdom::new(&container, Cached::new(CountRenders::new())));
123+ let vdom1 = vdom0.clone();
124+ let _vdom2 = vdom0.clone();
125+
126+ let clone_node_count_before = get_clone_node_count();
127+
128+ // Render something entirely different...
129+ vdom0
130+ .weak()
131+ .set_component(Box::new(RenderFn(|_| text("hi"))) as Box<dyn RootRender>)
132+ .await
133+ .unwrap();
134+
135+ // Re-render our cached node. Since we are creating it from scratch,
136+ // and already have a template from the earlier render, we should
137+ // call `cloneNode` here.
138+ vdom1
139+ .weak()
140+ .set_component(Box::new(Cached::new(CountRenders::new())) as Box<dyn RootRender>)
141+ .await
142+ .unwrap();
143+
144+ let clone_node_count_after = get_clone_node_count();
145+ assert_eq!(clone_node_count_after, clone_node_count_before + 1);
146+}
147+
148+struct Id(&'static str);
149+
150+impl Default for Id {
151+ fn default() -> Id {
152+ Id("")
153+ }
154+}
155+
156+impl<'a> Render<'a> for Id {
157+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
158+ text(self.0)
159+ }
160+}
161+
162+thread_local! {
163+ static WARM_CHEESE: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheese")));
164+ static WARM_CHEESIER: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheesier")));
165+}
166+
167+fn warm_cheese<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
168+ WARM_CHEESE.with(|c| {
169+ let _ = c.render(cx);
170+ c.render(cx)
171+ })
172+}
173+
174+fn warm_cheesier<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
175+ WARM_CHEESIER.with(|c| {
176+ let _ = c.render(cx);
177+ c.render(cx)
178+ })
179+}
180+
181+before_after! {
182+ cold_cache_and_not_cached {
183+ before(cx) {
184+ Cached::new(Id("cheese")).render(cx)
185+ }
186+ after(cx) {
187+ Id("ravioli").render(cx)
188+ }
189+ }
190+
191+ not_cached_and_cold_cache {
192+ before(cx) {
193+ Id("ravioli").render(cx)
194+ }
195+ after(cx) {
196+ Cached::new(Id("cheese")).render(cx)
197+ }
198+ }
199+
200+ cold_cache_and_cold_cache_same {
201+ before(cx) {
202+ Cached::new(Id("cheese")).render(cx)
203+ }
204+ after(cx) {
205+ Cached::new(Id("cheese")).render(cx)
206+ }
207+ }
208+
209+ cold_cache_and_cold_cache_different {
210+ before(cx) {
211+ Cached::new(Id("cheese")).render(cx)
212+ }
213+ after(cx) {
214+ Cached::new(Id("ravioli")).render(cx)
215+ }
216+ }
217+
218+ warm_cache_and_not_cached {
219+ before(cx) {
220+ warm_cheese(cx)
221+ }
222+ after(cx) {
223+ Id("ravioli").render(cx)
224+ }
225+ }
226+
227+ not_cached_and_warm_cache {
228+ before(cx) {
229+ Id("ravioli").render(cx)
230+ }
231+ after(cx) {
232+ warm_cheese(cx)
233+ }
234+ }
235+
236+ warm_cache_and_warm_cache_same {
237+ before(cx) {
238+ warm_cheese(cx)
239+ }
240+ after(cx) {
241+ warm_cheese(cx)
242+ }
243+ }
244+
245+ warm_cache_and_warm_cache_different {
246+ before(cx) {
247+ warm_cheese(cx)
248+ }
249+ after(cx) {
250+ warm_cheesier(cx)
251+ }
252+ }
253+
254+ cold_cache_and_warm_cache_same {
255+ before(cx) {
256+ Cached::new(Id("cheese")).render(cx)
257+ }
258+ after(cx) {
259+ warm_cheese(cx)
260+ }
261+ }
262+
263+ cold_cache_and_warm_cache_different {
264+ before(cx) {
265+ Cached::new(Id("cheese")).render(cx)
266+ }
267+ after(cx) {
268+ warm_cheesier(cx)
269+ }
270+ }
271+
272+ warm_cache_and_cold_cache_same {
273+ before(cx) {
274+ warm_cheese(cx)
275+ }
276+ after(cx) {
277+ Cached::new(Id("cheese")).render(cx)
278+ }
279+ }
280+
281+ warm_cache_and_cold_cache_different {
282+ before(cx) {
283+ warm_cheesier(cx)
284+ }
285+ after(cx) {
286+ Cached::new(Id("cheese")).render(cx)
287+ }
288+ }
101289}
tests/web/events.rs+58−156
@@ -1,7 +1,6 @@
11use super::create_element;
2−use bumpalo::Bump;
3−use dodrio::{Node, Render, Vdom};
4−use futures::Future;
2+use dodrio::{Node, Render, RenderContext, Vdom};
3+use futures::future::{select, Either};
54use std::cell::{Cell, RefCell};
65use std::rc::Rc;
76use wasm_bindgen::prelude::*;
@@ -10,7 +9,7 @@ use wasm_bindgen_test::*;
109
1110struct EventContainer {
1211 event: &'static str,
13− on_event: Box<FnMut()>,
12+ on_event: Box<dyn FnMut()>,
1413}
1514
1615impl EventContainer {
@@ -25,13 +24,10 @@ impl EventContainer {
2524 }
2625}
2726
28−impl Render for EventContainer {
29− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
30− where
31− 'a: 'bump,
32− {
27+impl<'a> Render<'a> for EventContainer {
28+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3329 use dodrio::builder::*;
34− div(bump)
30+ div(&cx)
3531 .attr("id", "target")
3632 .on(self.event, |root, _scheduler, _event| {
3733 (root.unwrap_mut::<EventContainer>().on_event)();
@@ -48,14 +44,14 @@ fn target(container: &web_sys::Element) -> web_sys::HtmlElement {
4844 .unchecked_into()
4945}
5046
51−#[wasm_bindgen_test(async)]
52−fn click() -> impl Future<Item = (), Error = JsValue> {
47+#[wasm_bindgen_test]
48+async fn click() {
5349 let container = create_element("div");
5450
55− let (sender, receiver) = futures::sync::oneshot::channel();
51+ let (sender, receiver) = futures::channel::oneshot::channel();
5652 let mut sender = Some(sender);
5753
58− let vdom = Vdom::new(
54+ let _vdom = Vdom::new(
5955 &container,
6056 EventContainer::new("click", move || {
6157 sender
@@ -68,18 +64,14 @@ fn click() -> impl Future<Item = (), Error = JsValue> {
6864
6965 target(&container).click();
7066
71− receiver
72− .map(|_| {
73− drop(vdom);
74− })
75− .map_err(|e| JsValue::from(e.to_string()))
67+ receiver.await.unwrap();
7668}
7769
78−#[wasm_bindgen_test(async)]
79−fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
70+#[wasm_bindgen_test]
71+async fn updated_listener_is_called() {
8072 let container = create_element("div");
8173
82− let (first_sender, first_receiver) = futures::sync::oneshot::channel();
74+ let (first_sender, first_receiver) = futures::channel::oneshot::channel();
8375 let mut first_sender = Some(first_sender);
8476
8577 let vdom = Vdom::new(
@@ -93,7 +85,7 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
9385 }),
9486 );
9587
96− let (second_sender, second_receiver) = futures::sync::oneshot::channel();
88+ let (second_sender, second_receiver) = futures::channel::oneshot::channel();
9789 let mut second_sender = Some(second_sender);
9890
9991 vdom.weak()
@@ -104,25 +96,22 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
10496 .send("second")
10597 .expect_throw("should not have dropped the receiver");
10698 })))
107− .map_err(|e| JsValue::from(e.to_string()))
108− .and_then(move |_| {
109− target(&container).click();
110−
111− first_receiver
112− .select(second_receiver)
113− .map(|(which, _other)| which)
114− .or_else(|(_error, other)| other)
115− .map(|which| {
116− assert_eq!(which, "second");
117− drop(vdom);
118− })
119− .map_err(|e| JsValue::from(e.to_string()))
120− })
99+ .await
100+ .unwrap();
101+
102+ target(&container).click();
103+
104+ match select(first_receiver, second_receiver).await {
105+ Either::Left((Ok(_), _)) => panic!(),
106+ Either::Left((Err(_), second)) => assert_eq!(second.await, Ok("second")),
107+ Either::Right((Ok(msg), _)) => assert_eq!(msg, "second"),
108+ Either::Right((Err(_), _)) => panic!(),
109+ }
121110}
122111
123112struct ListensOnlyOnFirstRender {
124113 count: Cell<usize>,
125− callback: Box<FnMut()>,
114+ callback: Box<dyn FnMut()>,
126115}
127116
128117impl ListensOnlyOnFirstRender {
@@ -137,17 +126,14 @@ impl ListensOnlyOnFirstRender {
137126 }
138127}
139128
140−impl Render for ListensOnlyOnFirstRender {
141− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
142− where
143− 'a: 'bump,
144− {
129+impl<'a> Render<'a> for ListensOnlyOnFirstRender {
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
145131 use dodrio::builder::*;
146132
147133 let count = self.count.get();
148134 self.count.set(count + 1);
149135
150− let mut elem = div(bump).attr("id", "target");
136+ let mut elem = div(&cx).attr("id", "target");
151137 if count == 0 {
152138 elem = elem.on("click", |root, _scheduler, _event| {
153139 (root.unwrap_mut::<ListensOnlyOnFirstRender>().callback)();
@@ -157,11 +143,11 @@ impl Render for ListensOnlyOnFirstRender {
157143 }
158144}
159145
160−#[wasm_bindgen_test(async)]
161−fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
146+#[wasm_bindgen_test]
147+async fn removed_listener_is_not_called() {
162148 let container = create_element("div");
163149
164− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
150+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
165151 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
166152 let outer_listener = Closure::wrap(Box::new(move |_| {
167153 outer_sender
@@ -170,13 +156,13 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
170156 .expect_throw("should only invoke outer_listener once")
171157 .send("outer")
172158 .expect_throw("should not have dropped receiver");
173− }) as Box<FnMut(web_sys::Event)>);
159+ }) as Box<dyn FnMut(web_sys::Event)>);
174160
175161 container
176162 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
177163 .unwrap();
178164
179− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
165+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
180166 let mut vdom_sender = Some(vdom_sender);
181167
182168 // Render a vdom with a listener into container.
@@ -191,33 +177,26 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
191177 }),
192178 );
193179
194− vdom.weak()
195− // Re-render, so we aren't listening anymore.
196− .render()
197− .map_err(|e| JsValue::from(e.to_string()))
198− .and_then(move |_| {
199− target(&container).click();
200−
201− // We should get our container's event handler fired, and not the unmounted
202− // vdom's event handler.
203− outer_receiver
204− .select(vdom_receiver)
205− .map(|(which, _other)| which)
206− .or_else(|(_error, other)| other)
207− .map(|which| {
208− assert_eq!(which, "outer");
209− drop(outer_listener);
210− drop(vdom)
211− })
212− .map_err(|e| JsValue::from(e.to_string()))
213− })
180+ // Re-render, so we aren't listening anymore.
181+ vdom.weak().render().await.unwrap();
182+
183+ target(&container).click();
184+
185+ // We should get our container's event handler fired, and not the unmounted
186+ // vdom's event handler.
187+ match select(outer_receiver, vdom_receiver).await {
188+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
189+ Either::Left((Err(_), _)) => panic!(),
190+ Either::Right((Ok(_), _)) => panic!(),
191+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
192+ }
214193}
215194
216−#[wasm_bindgen_test(async)]
217−fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsValue> {
195+#[wasm_bindgen_test]
196+async fn event_does_not_fire_after_unmounting() {
218197 let container = create_element("div");
219198
220− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
199+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
221200 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
222201 let outer_listener = Closure::wrap(Box::new(move |_| {
223202 outer_sender
@@ -226,13 +205,13 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
226205 .expect_throw("should only invoke outer_listener once")
227206 .send("outer")
228207 .expect_throw("should not have dropped receiver");
229− }) as Box<FnMut(web_sys::Event)>);
208+ }) as Box<dyn FnMut(web_sys::Event)>);
230209
231210 container
232211 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
233212 .unwrap();
234213
235− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
214+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
236215 let mut vdom_sender = Some(vdom_sender);
237216
238217 // Render a vdom with a listener into container.
@@ -262,87 +241,10 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
262241
263242 // We should get our container's event handler fired, and not the unmounted
264243 // vdom's event handler.
265− outer_receiver
266− .select(vdom_receiver)
267− .map(|(which, _other)| which)
268− .or_else(|(_error, other)| other)
269− .map(|which| {
270− assert_eq!(which, "outer");
271− drop(outer_listener);
272− })
273− .map_err(|e| JsValue::from(e.to_string()))
274−}
275−
276−struct CountRendersAndListen {
277− count: Cell<usize>,
278− callback: Box<FnMut(usize)>,
279−}
280−
281−impl CountRendersAndListen {
282− fn new<F>(callback: F) -> CountRendersAndListen
283− where
284− F: 'static + FnMut(usize),
285− {
286− CountRendersAndListen {
287− count: Cell::new(0),
288− callback: Box::new(callback),
289− }
290− }
291−}
292−
293−impl Render for CountRendersAndListen {
294− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
295− where
296− 'a: 'bump,
297− {
298− use dodrio::builder::*;
299−
300− let count = self.count.get() + 1;
301− self.count.set(count);
302−
303− button(bump)
304− .attr("id", "target")
305− .on("click", |comp, _vdom, _event| {
306− let comp: &mut _ = comp.unwrap_mut::<dodrio::Cached<CountRendersAndListen>>();
307− let comp: &mut CountRendersAndListen = comp;
308− let n = comp.count.get();
309− (comp.callback)(n);
310− })
311− .finish()
244+ match select(outer_receiver, vdom_receiver).await {
245+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
246+ Either::Left((Err(_), _)) => panic!(),
247+ Either::Right((Ok(_), _)) => panic!(),
248+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
312249 }
313250}
314−
315−#[wasm_bindgen_test(async)]
316−fn cached_listeners_work_correctly() -> impl Future<Item = (), Error = JsValue> {
317− let container = create_element("div");
318−
319− let (sender, receiver) = futures::sync::oneshot::channel();
320− let mut sender = Some(sender);
321− let vdom = Vdom::new(
322− &container,
323− dodrio::Cached::new(CountRendersAndListen::new(move |n| {
324− sender.take().unwrap().send(n).unwrap();
325− })),
326− );
327−
328− let weak0 = vdom.weak();
329− let weak1 = vdom.weak();
330− let weak2 = vdom.weak();
331− vdom.weak()
332− // Re-render a few times, so we are using the cached version.
333− .render()
334− .and_then(move |_| weak0.render())
335− .and_then(move |_| weak1.render())
336− .and_then(move |_| weak2.render())
337− .map_err(|e| JsValue::from(e.to_string()))
338− .and_then(move |_| {
339− target(&container).click();
340−
341− receiver
342− .map(|n| {
343− assert_eq!(n, 1);
344− drop(vdom);
345− })
346− .map_err(|e| JsValue::from(e.to_string()))
347− })
348−}
tests/web/js_api.rs+37−47
@@ -1,8 +1,7 @@
11use super::{assert_rendered, create_element, RenderFn};
2−use dodrio::{builder::*, bumpalo::Bump, Node, Render, Vdom};
2+use dodrio::{builder::*, Node, Render, RenderContext, Vdom};
33use dodrio_js_api::JsRender;
4−use futures::prelude::*;
5−use futures::sync::oneshot;
4+use futures::channel::oneshot;
65use js_sys::Object;
76use std::rc::Rc;
87use wasm_bindgen::prelude::*;
@@ -14,7 +13,7 @@ pub struct WrapJs {
1413}
1514
1615impl WrapJs {
17− pub fn new() -> (WrapJs, oneshot::Receiver<()>, Closure<FnMut()>) {
16+ pub fn new() -> (WrapJs, oneshot::Receiver<()>, Closure<dyn FnMut()>) {
1817 let (sender, receiver) = oneshot::channel();
1918
2019 let on_after_render = Closure::wrap(Box::new({
@@ -26,7 +25,7 @@ impl WrapJs {
2625 .send(())
2726 .expect_throw("receiver should not have been dropped");
2827 }
29− }) as Box<FnMut()>);
28+ }) as Box<dyn FnMut()>);
3029
3130 let component = WrapJs {
3231 inner: JsRender::new(JsComponent::new(&on_after_render)),
@@ -36,14 +35,11 @@ impl WrapJs {
3635 }
3736}
3837
39−impl Render for WrapJs {
40− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
41− where
42− 'a: 'bump,
43− {
44− div(bump)
38+impl<'a> Render<'a> for WrapJs {
39+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
40+ div(&cx)
4541 .attr("class", "wrap-js")
46− .children([self.inner.render(bump)])
42+ .children([self.inner.render(cx)])
4743 .finish()
4844 }
4945}
@@ -55,7 +51,7 @@ extern "C" {
5551 type JsComponent;
5652
5753 #[wasm_bindgen(constructor)]
58− fn new(on_after_render: &Closure<FnMut()>) -> JsComponent;
54+ fn new(on_after_render: &Closure<dyn FnMut()>) -> JsComponent;
5955}
6056
6157fn eval_js_rendering_component() {
@@ -99,24 +95,24 @@ fn eval_js_rendering_component() {
9995 });
10096}
10197
102−#[wasm_bindgen_test(async)]
103−fn can_use_js_rendering_components() -> impl Future<Item = (), Error = JsValue> {
98+#[wasm_bindgen_test]
99+async fn can_use_js_rendering_components() {
104100 eval_js_rendering_component();
105101
106102 let container = create_element("div");
107− let (component, receiver, closure) = WrapJs::new();
103+ let (component, receiver, _closure) = WrapJs::new();
104+ let _vdom = Rc::new(Vdom::new(&container, component));
108105
109− let vdom = Rc::new(Vdom::new(&container, component));
110106 assert_rendered(
111107 &container,
112− &RenderFn(|bump| {
113− div(bump)
108+ &RenderFn(|cx| {
109+ div(&cx)
114110 .attr("class", "wrap-js")
115− .children([span(bump)
111+ .children([span(&cx)
116112 .attr("class", "js-component")
117113 .children([
118114 text("Here is some plain text"),
119− b(bump)
115+ b(&cx)
120116 .children([text("...and here is some bold text")])
121117 .finish(),
122118 text("0"),
@@ -134,30 +130,24 @@ fn can_use_js_rendering_components() -> impl Future<Item = (), Error = JsValue>
134130 .expect_throw("should be an `HTMLElement` object")
135131 .click();
136132
137− receiver
138− .map(move |_| {
139− assert_rendered(
140− &container,
141− &RenderFn(|bump| {
142− div(bump)
143− .attr("class", "wrap-js")
144− .children([span(bump)
145− .attr("class", "js-component")
146− .children([
147− text("Here is some plain text"),
148− b(bump)
149− .children([text("...and here is some bold text")])
150− .finish(),
151− text("1"),
152− ])
153− .finish()])
154− .finish()
155− }),
156− );
157−
158− drop(vdom);
159− drop(container);
160− drop(closure);
161− })
162− .map_err(|e| e.to_string().into())
133+ receiver.await.unwrap();
134+
135+ assert_rendered(
136+ &container,
137+ &RenderFn(|cx| {
138+ div(&cx)
139+ .attr("class", "wrap-js")
140+ .children([span(&cx)
141+ .attr("class", "js-component")
142+ .children([
143+ text("Here is some plain text"),
144+ b(&cx)
145+ .children([text("...and here is some bold text")])
146+ .finish(),
147+ text("1"),
148+ ])
149+ .finish()])
150+ .finish()
151+ }),
152+ );
163153}
tests/web/keyed.jsnew file+25
@@ -0,0 +1,25 @@
1+export function saveKeyedElements(container) {
2+ const saved = new Map;
3+ const children = container.querySelectorAll(".keyed");
4+ for (const child of children) {
5+ saved.set(child.id, child);
6+ }
7+ return saved;
8+}
9+
10+function assertEq(a, b, msg) {
11+ if (a !== b) {
12+ throw new Error(`assertEq failed: ${a} !== ${b}; ${msg}`);
13+ }
14+}
15+
16+export function checkKeyedElements(container, saved) {
17+ const children = container.querySelectorAll(".keyed");
18+ for (const child of children) {
19+ console.log(`checking child=${child.outerHTML}`);
20+ const original = saved.get(child.id);
21+ if (original) {
22+ assertEq(original, child, `did not preserve child with key=${child.id}`);
23+ }
24+ }
25+}
tests/web/keyed.rsnew file+269
@@ -0,0 +1,269 @@
1+use crate::{assert_rendered, create_element};
2+use dodrio::{builder::*, bumpalo, Node, Render, RenderContext, Vdom};
3+use log::*;
4+use std::rc::Rc;
5+use wasm_bindgen::prelude::*;
6+use wasm_bindgen_test::*;
7+
8+struct Keyed(u16);
9+
10+impl<'a> Render<'a> for Keyed {
11+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
12+ let key = bumpalo::format!(in cx.bump, "{}", self.0).into_bump_str();
13+ div(&cx)
14+ .attr("class", "keyed")
15+ .attr("id", key)
16+ .key(self.0 as u32)
17+ .finish()
18+ }
19+}
20+
21+fn keyed<'a, Keys>(cx: &mut RenderContext<'a>, keys: Keys) -> Node<'a>
22+where
23+ Keys: AsRef<[u16]>,
24+{
25+ let mut parent = div(&cx).attr("class", "parent");
26+
27+ for &k in keys.as_ref() {
28+ parent = parent.child(Keyed(k).render(cx));
29+ }
30+
31+ parent.finish()
32+}
33+
34+async fn assert_keyed<Before, After>(before: Before, after: After) -> Result<(), JsValue>
35+where
36+ Before: 'static + for<'a> Render<'a>,
37+ After: 'static + for<'a> Render<'a>,
38+{
39+ #[wasm_bindgen(module = "/tests/web/keyed.js")]
40+ extern "C" {
41+ #[wasm_bindgen(js_name = saveKeyedElements)]
42+ fn save_keyed_elements(container: &web_sys::Element) -> JsValue;
43+
44+ #[wasm_bindgen(js_name = checkKeyedElements)]
45+ fn check_keyed_elements(container: &web_sys::Element, saved: JsValue);
46+ }
47+
48+ let container = create_element("div");
49+
50+ let before = Rc::new(before);
51+ let after = Rc::new(after);
52+
53+ debug!("====== Rendering the *before* DOM into the physical DOM ======");
54+ let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
55+ let _vdom2 = vdom1.clone();
56+ let saved = save_keyed_elements(&container);
57+
58+ debug!("====== Checking the *before* DOM against the physical DOM ======");
59+ assert_rendered(&container, &before);
60+
61+ debug!("====== Rendering the *after* DOM into the physical DOM ======");
62+ let weak = vdom1.weak();
63+ weak.set_component(Box::new(after.clone()))
64+ .await
65+ .map_err(|e| e.to_string())?;
66+
67+ debug!("====== Checking the *after* DOM against the physical DOM ======");
68+ assert_rendered(&container, &after);
69+ check_keyed_elements(&container, saved);
70+
71+ Ok(())
72+}
73+
74+macro_rules! keyed_tests {
75+ ( $(
76+ $name:ident {
77+ before($before_cx:ident) {
78+ $( $before:tt )*
79+ }
80+ after($after_cx:ident) {
81+ $( $after:tt )*
82+ }
83+ }
84+ )* ) => {
85+ $(
86+ #[wasm_bindgen_test]
87+ async fn $name() {
88+ use crate::RenderFn;
89+ log::debug!("############### {} ###############", stringify!($name));
90+ assert_keyed(
91+ RenderFn(|$before_cx| { $( $before )* }),
92+ RenderFn(|$after_cx| { $( $after )* }),
93+ )
94+ .await
95+ .unwrap()
96+ }
97+ )*
98+ }
99+}
100+
101+keyed_tests! {
102+ same_order {
103+ before(cx) {
104+ keyed(cx, [1, 2, 3])
105+ }
106+ after(cx) {
107+ keyed(cx, [1, 2, 3])
108+ }
109+ }
110+
111+ same_order_append {
112+ before(cx) {
113+ keyed(cx, [1])
114+ }
115+ after(cx) {
116+ keyed(cx, [1, 2])
117+ }
118+ }
119+
120+ same_order_delete {
121+ before(cx) {
122+ keyed(cx, [1, 2, 3])
123+ }
124+ after(cx) {
125+ keyed(cx, [1, 2])
126+ }
127+ }
128+
129+ same_suffix {
130+ before(cx) {
131+ keyed(cx, [1, 2, 3])
132+ }
133+ after(cx) {
134+ keyed(cx, [4, 2, 3])
135+ }
136+ }
137+
138+ same_prefix_and_suffix_reorder_middle {
139+ before(cx) {
140+ keyed(cx, [1, 2, 3, 4])
141+ }
142+ after(cx) {
143+ keyed(cx, [1, 3, 2, 4])
144+ }
145+ }
146+
147+ same_prefix_and_suffix_new_middle {
148+ before(cx) {
149+ keyed(cx, [2, 3, 4, 5])
150+ }
151+ after(cx) {
152+ keyed(cx, [2, 7, 8, 5])
153+ }
154+ }
155+
156+ reverse_order {
157+ before(cx) {
158+ keyed(cx, [1, 2, 3])
159+ }
160+ after(cx) {
161+ keyed(cx, [3, 2, 1])
162+ }
163+ }
164+
165+ no_shared_keys {
166+ before(cx) {
167+ keyed(cx, [1, 2])
168+ }
169+ after(cx) {
170+ keyed(cx, [3, 4])
171+ }
172+ }
173+
174+ new_keys_in_middle {
175+ before(cx) {
176+ keyed(cx, [1, 2])
177+ }
178+ after(cx) {
179+ keyed(cx, [1, 3, 4, 2])
180+ }
181+ }
182+
183+ new_keys_at_start_and_end {
184+ before(cx) {
185+ keyed(cx, [1, 2, 3])
186+ }
187+ after(cx) {
188+ keyed(cx, [4, 1, 2, 3, 5])
189+ }
190+ }
191+
192+ delete_prefix {
193+ before(cx) {
194+ keyed(cx, [1, 2, 3])
195+ }
196+ after(cx) {
197+ keyed(cx, [2, 3])
198+ }
199+ }
200+
201+ delete_suffix {
202+ before(cx) {
203+ keyed(cx, [1, 2, 3])
204+ }
205+ after(cx) {
206+ keyed(cx, [1, 2])
207+ }
208+ }
209+
210+ delete_middle {
211+ before(cx) {
212+ keyed(cx, [1, 2, 3])
213+ }
214+ after(cx) {
215+ keyed(cx, [1, 3])
216+ }
217+ }
218+
219+ nested_keyed_children {
220+ before(cx) {
221+ ul(&cx)
222+ .children([
223+ li(&cx)
224+ .key(1)
225+ .children([
226+ keyed(cx, [2, 3, 4])
227+ ])
228+ .finish(),
229+ li(&cx)
230+ .key(5)
231+ .children([
232+ keyed(cx, [6, 7, 8])
233+ ])
234+ .finish(),
235+ li(&cx)
236+ .key(9)
237+ .children([
238+ keyed(cx, [10, 11, 12])
239+ ])
240+ .finish(),
241+ ])
242+ .finish()
243+ }
244+ after(cx) {
245+ ul(&cx)
246+ .children([
247+ li(&cx)
248+ .key(9)
249+ .children([
250+ keyed(cx, [12, 11, 10])
251+ ])
252+ .finish(),
253+ li(&cx)
254+ .key(5)
255+ .children([
256+ keyed(cx, [8, 7, 6])
257+ ])
258+ .finish(),
259+ li(&cx)
260+ .key(1)
261+ .children([
262+ keyed(cx, [4, 3, 2])
263+ ])
264+ .finish(),
265+ ])
266+ .finish()
267+ }
268+ }
269+}
tests/web/main.rs+60−37
@@ -1,11 +1,14 @@
11//! Test suite for the Web and headless browsers.
22
3−#![cfg(target_arch = "wasm32")]
3+#![cfg(all(feature = "xxx-unstable-internal-use-only", target_arch = "wasm32"))]
44
55use bumpalo::Bump;
6−use dodrio::{Attribute, Node, Render, Vdom};
7−use futures::prelude::*;
6+use dodrio::{
7+ Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
8+};
9+use fxhash::FxHashMap;
810use log::*;
11+use std::cell::RefCell;
912use std::rc::Rc;
1013use wasm_bindgen::prelude::*;
1114use wasm_bindgen::JsCast;
@@ -16,6 +19,7 @@ wasm_bindgen_test_configure!(run_in_browser);
1619pub mod cached;
1720pub mod events;
1821pub mod js_api;
22+pub mod keyed;
1923pub mod render;
2024
2125pub fn window() -> web_sys::Window {
@@ -46,15 +50,20 @@ pub fn init_logging() {
4650
4751/// Assert that the `container` contains the physical DOM tree that matches
4852/// `r`'s rendered virtual DOM.
49−pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
53+pub fn assert_rendered<R: for<'a> Render<'a>>(container: &web_sys::Element, r: &R) {
5054 init_logging();
5155
56+ let cached_set = &RefCell::new(CachedSet::default());
5257 let bump = &Bump::new();
53− let node = r.render(bump);
58+ let templates = &mut FxHashMap::default();
59+ let cx = &mut RenderContext::new(bump, cached_set, templates);
60+ let node = r.render(cx);
5461 let child = container
5562 .first_child()
5663 .expect("container does not have anything rendered into it?");
57− check_node(&child, &node);
64+
65+ let cached_set = cached_set.borrow();
66+ check_node(&cached_set, &child, &node);
5867
5968 fn stringify_actual_node(n: &web_sys::Node) -> String {
6069 if let Some(el) = n.dyn_ref::<web_sys::Element>() {
@@ -64,12 +73,12 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
6473 }
6574 }
6675
67− fn check_node(actual: &web_sys::Node, expected: &Node) {
76+ fn check_node(cached_set: &CachedSet, actual: &web_sys::Node, expected: &Node) {
6877 debug!("check_render:");
6978 debug!(" actual = {}", stringify_actual_node(&actual));
7079 debug!(" expected = {:#?}", expected);
71− match expected {
72− Node::Text(text_node) => {
80+ match expected.kind {
81+ NodeKind::Text(TextNode { text }) => {
7382 assert_eq!(
7483 actual.node_name().to_uppercase(),
7584 "#TEXT",
@@ -77,21 +86,34 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
7786 );
7887 assert_eq!(
7988 actual.text_content().unwrap_or_default(),
80− text_node.text(),
89+ text,
8190 "actual.text_content() == expected.text()"
8291 );
8392 }
84− Node::Element(elem) => {
93+ NodeKind::Element(&ElementNode {
94+ tag_name,
95+ attributes,
96+ children,
97+ namespace,
98+ ..
99+ }) => {
85100 assert_eq!(
86101 actual.node_name().to_uppercase(),
87− elem.tag_name().to_uppercase(),
102+ tag_name.to_uppercase(),
88103 "actual.node_name() == expected.tag_name()"
89104 );
90105 let actual = actual
91106 .dyn_ref::<web_sys::Element>()
92107 .expect("`actual` should be an `Element`");
93− check_attributes(actual.attributes(), elem.attributes());
94− check_children(actual.child_nodes(), elem.children());
108+ check_attributes(actual.attributes(), attributes);
109+ check_children(cached_set, actual.child_nodes(), children);
110+ if let Some(namespace) = namespace {
111+ assert_eq!(actual.namespace_uri(), Some(namespace.into()))
112+ }
113+ }
114+ NodeKind::Cached(ref c) => {
115+ let (expected, _template) = cached_set.get(c.id);
116+ check_node(cached_set, actual, &expected);
95117 }
96118 }
97119 }
@@ -115,7 +137,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
115137 }
116138 }
117139
118− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
140+ fn check_children(cached_set: &CachedSet, actual: web_sys::NodeList, expected: &[Node]) {
119141 assert_eq!(
120142 actual.length(),
121143 expected.len() as u32,
@@ -123,7 +145,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
123145 );
124146 for (i, child) in expected.iter().enumerate() {
125147 let actual_child = actual.item(i as u32).unwrap();
126− check_node(&actual_child, child);
148+ check_node(cached_set, &actual_child, child);
127149 }
128150 }
129151}
@@ -131,27 +153,24 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
131153/// Use the function `F` to render.
132154pub struct RenderFn<F>(F)
133155where
134− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
156+ F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>;
135157
136−impl<F> Render for RenderFn<F>
158+impl<'a, F> Render<'a> for RenderFn<F>
137159where
138− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
160+ F: for<'b> Fn(&mut RenderContext<'b>) -> Node<'b>,
139161{
140− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
141− where
142− 'a: 'bump,
143− {
144− (self.0)(bump)
162+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
163+ (self.0)(cx)
145164 }
146165}
147166
148167/// Assert that if we start by rendering the `before` virtual DOM tree into a
149168/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
150169/// the physical DOM tree correctly matches `after`.
151−pub fn assert_before_after<R, S>(before: R, after: S) -> impl Future<Item = (), Error = JsValue>
170+pub async fn assert_before_after<R, S>(before: R, after: S) -> Result<(), JsValue>
152171where
153− R: 'static + Render,
154− S: 'static + Render,
172+ R: 'static + for<'a> Render<'a>,
173+ S: 'static + for<'a> Render<'a>,
155174{
156175 let container = create_element("div");
157176
@@ -160,7 +179,7 @@ where
160179
161180 debug!("====== Rendering the *before* DOM into the physical DOM ======");
162181 let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
163− let vdom2 = vdom1.clone();
182+ let _vdom2 = vdom1.clone();
164183
165184 debug!("====== Checking the *before* DOM against the physical DOM ======");
166185 assert_rendered(&container, &before);
@@ -168,15 +187,16 @@ where
168187 debug!("====== Rendering the *after* DOM into the physical DOM ======");
169188 let weak = vdom1.weak();
170189 weak.set_component(Box::new(after.clone()))
171− .map(move |_| {
172− debug!("====== Checking the *after* DOM against the physical DOM ======");
173− assert_rendered(&container, &after);
174− drop(vdom2);
175− })
176− .map_err(|e| JsValue::from(e.to_string()))
190+ .await
191+ .map_err(|e| e.to_string())?;
192+
193+ debug!("====== Checking the *after* DOM against the physical DOM ======");
194+ assert_rendered(&container, &after);
195+
196+ Ok(())
177197}
178198
179−/// A helper macro for decalaring a bunch of `assert_before_after` tests.
199+/// A helper macro for declaring a bunch of `assert_before_after` tests.
180200#[macro_export]
181201macro_rules! before_after {
182202 ( $(
@@ -190,13 +210,16 @@ macro_rules! before_after {
190210 }
191211 )* ) => {
192212 $(
193− #[wasm_bindgen_test(async)]
194− fn $name() -> impl Future<Item = (), Error = wasm_bindgen::JsValue> {
213+ #[wasm_bindgen_test]
214+ async fn $name() {
195215 use crate::{assert_before_after, RenderFn};
216+ log::debug!("############### {} ###############", stringify!($name));
196217 assert_before_after(
197218 RenderFn(|$before_bump| { $( $before )* }),
198219 RenderFn(|$after_bump| { $( $after )* })
199220 )
221+ .await
222+ .unwrap()
200223 }
201224 )*
202225 }
tests/web/render.rs+183−83
@@ -1,12 +1,12 @@
11use super::{assert_rendered, before_after, create_element, RenderFn};
2−use dodrio::{builder::*, Vdom};
3−use futures::prelude::*;
2+use dodrio::{builder::*, bumpalo::collections::String, Node, Render, RenderContext, Vdom};
43use std::rc::Rc;
4+use wasm_bindgen::{ JsCast};
55use wasm_bindgen_test::*;
66
77#[wasm_bindgen_test]
88fn render_initial_text() {
9− let hello = Rc::new(RenderFn(|_bump| text("hello")));
9+ let hello = Rc::new(RenderFn(|_cx| text("hello")));
1010
1111 let container = create_element("div");
1212 let _vdom = Vdom::new(&container, hello.clone());
@@ -15,10 +15,10 @@ fn render_initial_text() {
1515
1616#[wasm_bindgen_test]
1717fn render_initial_node() {
18− let hello = Rc::new(RenderFn(|bump| {
19− div(bump)
18+ let hello = Rc::new(RenderFn(|cx| {
19+ div(&cx)
2020 .attr("id", "hello-world")
21− .children([text("Hello "), span(bump).child(text("World!")).finish()])
21+ .children([text("Hello "), span(&cx).child(text("World!")).finish()])
2222 .finish()
2323 }));
2424
@@ -30,96 +30,183 @@ fn render_initial_node() {
3030#[wasm_bindgen_test]
3131fn container_is_emptied_upon_drop() {
3232 let container = create_element("div");
33− let vdom = Vdom::new(&container, RenderFn(|_bump| text("blah")));
33+ let vdom = Vdom::new(&container, RenderFn(|_cx| text("blah")));
3434 drop(vdom);
3535 assert!(container.first_child().is_none());
3636}
3737
38+/// Renders a child with a lifetime scoped to the RenderContext bump arena.
39+#[wasm_bindgen_test]
40+fn render_bump_scoped_node() {
41+ struct Child<'a> {
42+ name: &'a str,
43+ }
44+
45+ impl<'a> Render<'a> for Child<'a> {
46+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
47+ text(self.name)
48+ }
49+ }
50+
51+ struct Parent;
52+
53+ impl<'a> Render<'a> for Parent {
54+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
55+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
56+
57+ div(&cx)
58+ .children([Child { name: child_name }.render(cx)])
59+ .finish()
60+ }
61+ }
62+
63+ let parent = Rc::new(RenderFn(|cx| {
64+ Parent.render(cx)
65+ }));
66+
67+ let container = create_element("div");
68+ let _vdom = Vdom::new(&container, parent.clone());
69+
70+ assert_rendered(&container, &parent);
71+}
72+
73+/// Originally, dodrio would use the className property for SVGs.
74+///
75+/// This is problematic because when SVG elements are created, the className is flagged as a read
76+/// only property, so setting it causes an exception to be thrown. Here's an example of how this
77+/// happens:
78+///
79+/// let elem = web_sys::window()
80+/// .unwrap()
81+/// .document()
82+/// .unwrap()
83+/// .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg")
84+/// .unwrap();
85+///
86+/// elem.set_class_name("does-not-work");
87+///
88+/// -----------------------------------------------------------------------------------------------
89+///
90+/// wasm-bindgen: imported JS function that was not marked as `catch` threw an error:
91+/// setting getter-only property "className"
92+///
93+/// -----------------------------------------------------------------------------------------------
94+///
95+/// Now, dodrio passes the 'class' attribute of all namespaced elements into set_attribute. This
96+/// satisfies the restrictions on SVG and keeps the optimized path for non-namespaced elements
97+#[wasm_bindgen_test(async)]
98+async fn test_svg_set_class() {
99+ let container = create_element("div");
100+
101+
102+ let valid_svg = Rc::new(RenderFn(|cx| {
103+ ElementBuilder::new(cx.bump, "svg")
104+ .namespace(Some("http://www.w3.org/2000/svg"))
105+ .attr("class", "works")
106+ .finish()
107+ }));
108+
109+ let vdom = Vdom::new(&container, valid_svg.clone());
110+ let weak = vdom.weak();
111+
112+ weak.render().await.unwrap();
113+
114+ assert_eq!(
115+ "works",
116+ container.first_child()
117+ .expect("unable to get svg")
118+ .dyn_ref::<web_sys::Element>()
119+ .expect("svg should be an element")
120+ .get_attribute("class")
121+ .expect("unable to get 'class' of svg")
122+ );
123+}
124+
38125before_after! {
39126 same_text {
40− before(_bump) {
127+ before(_cx) {
41128 text("hello")
42129 }
43− after(_bump) {
130+ after(_cx) {
44131 text("hello")
45132 }
46133 }
47134
48135 update_text {
49− before(_bump) {
136+ before(_cx) {
50137 text("before")
51138 }
52− after(_bump) {
139+ after(_cx) {
53140 text("after")
54141 }
55142 }
56143
57144 replace_text_with_elem {
58− before(_bump) {
145+ before(_cx) {
59146 text("before")
60147 }
61− after(bump) {
62− div(bump).finish()
148+ after(cx) {
149+ div(&cx).finish()
63150 }
64151 }
65152
66153 replace_elem_with_text {
67− before(bump) {
68− div(bump).finish()
154+ before(cx) {
155+ div(&cx).finish()
69156 }
70− after(_bump) {
157+ after(_cx) {
71158 text("before")
72159 }
73160 }
74161
75162 same_elem {
76− before(bump) {
77− div(bump).finish()
163+ before(cx) {
164+ div(&cx).finish()
78165 }
79− after(bump) {
80− div(bump).finish()
166+ after(cx) {
167+ div(&cx).finish()
81168 }
82169 }
83170
84171 elems_with_different_tag_names {
85− before(bump) {
86− span(bump).finish()
172+ before(cx) {
173+ span(&cx).finish()
87174 }
88− after(bump) {
89− div(bump).finish()
175+ after(cx) {
176+ div(&cx).finish()
90177 }
91178 }
92179
93180 same_tag_name_update_attribute {
94− before(bump) {
95− div(bump).attr("value", "1").finish()
181+ before(cx) {
182+ div(&cx).attr("value", "1").finish()
96183 }
97− after(bump) {
98− div(bump).attr("value", "2").finish()
184+ after(cx) {
185+ div(&cx).attr("value", "2").finish()
99186 }
100187 }
101188
102189 same_tag_name_remove_attribute {
103− before(bump) {
104− div(bump).attr("value", "1").finish()
190+ before(cx) {
191+ div(&cx).attr("value", "1").finish()
105192 }
106− after(bump) {
107− div(bump).finish()
193+ after(cx) {
194+ div(&cx).finish()
108195 }
109196 }
110197
111198 same_tag_name_add_attribute {
112− before(bump) {
113− div(bump).finish()
199+ before(cx) {
200+ div(&cx).finish()
114201 }
115− after(bump) {
116− div(bump).attr("value", "2").finish()
202+ after(cx) {
203+ div(&cx).attr("value", "2").finish()
117204 }
118205 }
119206
120207 same_tag_name_many_attributes {
121− before(bump) {
122− div(bump)
208+ before(cx) {
209+ div(&cx)
123210 .attr("before-1", "1")
124211 .attr("shared-1", "1")
125212 .attr("modified-1", "1")
@@ -131,8 +218,8 @@ before_after! {
131218 .attr("modified-3", "3")
132219 .finish()
133220 }
134− after(bump) {
135− div(bump)
221+ after(cx) {
222+ div(&cx)
136223 .attr("after-1", "1")
137224 .attr("shared-1", "1")
138225 .attr("modified-1", "100")
@@ -147,97 +234,110 @@ before_after! {
147234 }
148235
149236 same_tag_same_children {
150− before(bump) {
151− div(bump).child(text("child")).finish()
237+ before(cx) {
238+ div(&cx).child(text("child")).finish()
152239 }
153− after(bump) {
154− div(bump).child(text("child")).finish()
240+ after(cx) {
241+ div(&cx).child(text("child")).finish()
155242 }
156243 }
157244
158245 same_tag_update_child {
159− before(bump) {
160− div(bump).child(text("before")).finish()
246+ before(cx) {
247+ div(&cx).child(text("before")).finish()
161248 }
162− after(bump) {
163− div(bump).child(text("after")).finish()
249+ after(cx) {
250+ div(&cx).child(text("after")).finish()
164251 }
165252 }
166253
167254 same_tag_add_child {
168− before(bump) {
169− div(bump).finish()
255+ before(cx) {
256+ div(&cx).finish()
170257 }
171− after(bump) {
172− div(bump).child(text("child")).finish()
258+ after(cx) {
259+ div(&cx).child(text("child")).finish()
173260 }
174261 }
175262
176263 same_tag_remove_child {
177− before(bump) {
178− div(bump).child(text("child")).finish()
264+ before(cx) {
265+ div(&cx).child(text("child")).finish()
179266 }
180− after(bump) {
181− div(bump).finish()
267+ after(cx) {
268+ div(&cx).finish()
182269 }
183270 }
184271
185272 same_tag_update_many_children {
186− before(bump) {
187− div(bump)
273+ before(cx) {
274+ div(&cx)
188275 .children([
189− div(bump).finish(),
190− span(bump).finish(),
191− p(bump).finish(),
276+ div(&cx).finish(),
277+ span(&cx).finish(),
278+ p(&cx).finish(),
192279 ])
193280 .finish()
194281 }
195− after(bump) {
196− div(bump)
282+ after(cx) {
283+ div(&cx)
197284 .children([
198− span(bump).finish(),
199− p(bump).finish(),
200− div(bump).finish(),
285+ span(&cx).finish(),
286+ p(&cx).finish(),
287+ div(&cx).finish(),
201288 ])
202289 .finish()
203290 }
204291 }
205292
206293 same_tag_remove_many_children {
207− before(bump) {
208− div(bump)
294+ before(cx) {
295+ div(&cx)
209296 .children([
210− div(bump).finish(),
211− span(bump).finish(),
212− p(bump).finish(),
297+ div(&cx).finish(),
298+ span(&cx).finish(),
299+ p(&cx).finish(),
213300 ])
214301 .finish()
215302 }
216− after(bump) {
217− div(bump)
303+ after(cx) {
304+ div(&cx)
218305 .children([
219− div(bump).finish(),
306+ div(&cx).finish(),
220307 ])
221308 .finish()
222309 }
223310 }
224311
225312 same_tag_add_many_children {
226− before(bump) {
227− div(bump)
313+ before(cx) {
314+ div(&cx)
228315 .children([
229− div(bump).finish(),
316+ div(&cx).finish(),
230317 ])
231318 .finish()
232319 }
233− after(bump) {
234− div(bump)
320+ after(cx) {
321+ div(&cx)
235322 .children([
236− div(bump).finish(),
237− span(bump).finish(),
238− p(bump).finish(),
323+ div(&cx).finish(),
324+ span(&cx).finish(),
325+ p(&cx).finish(),
239326 ])
240327 .finish()
241328 }
242329 }
330+
331+ same_tag_different_namespace {
332+ before(cx) {
333+ div(&cx)
334+ .namespace(Some("http://example.com"))
335+ .finish()
336+ }
337+ after(cx) {
338+ div(&cx)
339+ .namespace(Some("http://example.net"))
340+ .finish()
341+ }
342+ }
243343}