add notes and rust stuff
authorAndrea Cornell <anders@acorn.pw>
Thu, 21 Sep 2023 20:47:14 +0000 (16:47 -0400)
committerAndrea Cornell <anders@acorn.pw>
Thu, 21 Sep 2023 20:48:57 +0000 (16:48 -0400)
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
notes [new file with mode: 0644]
sharedmodel/Cargo.toml [new file with mode: 0644]
sharedmodel/src/html_nodes.rs [new file with mode: 0644]
sharedmodel/src/lib.rs [new file with mode: 0644]
sharedmodel/src/live_event_buffer.rs [new file with mode: 0644]
sharedmodel/src/main.rs [new file with mode: 0644]
sharedmodel_wasm/Cargo.toml [new file with mode: 0644]
sharedmodel_wasm/src/lib.rs [new file with mode: 0644]

diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..2bb85bc
--- /dev/null
@@ -0,0 +1,443 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "bumpalo"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "futf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.138"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
+
+[[package]]
+name = "log"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "cfg-if 0.1.10",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cdc457076c78ab54d5e0d6fa7c47981757f1e34dc39ff92787f217dede586c4"
+dependencies = [
+ "unreachable",
+]
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "getrandom",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
+
+[[package]]
+name = "serde"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharedmodel"
+version = "0.1.0"
+dependencies = [
+ "html5ever",
+ "string_cache",
+ "uuid",
+]
+
+[[package]]
+name = "sharedmodel_wasm"
+version = "0.1.0"
+dependencies = [
+ "js-sys",
+ "sharedmodel",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83da420ee8d1a89e640d0948c646c1c088758d3a3c538f943bfa97bdac17929d"
+
+[[package]]
+name = "string_cache"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a"
+dependencies = [
+ "lazy_static",
+ "new_debug_unreachable",
+ "phf_shared",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9de21546595a0873061940d994bbbc5c35f024ae4fd61ec5c5b159115684f508"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+dependencies = [
+ "void",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
+
+[[package]]
+name = "uuid"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..91d4007
--- /dev/null
@@ -0,0 +1,2 @@
+[workspace]
+members = ['sharedmodel', 'sharedmodel_wasm']
diff --git a/notes b/notes
new file mode 100644 (file)
index 0000000..0750516
--- /dev/null
+++ b/notes
@@ -0,0 +1,107 @@
+WebRTC
+
+https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Simple_RTCDataChannel_sample
+
+WebSocket SaaS
+https://github.com/ably/ably-js
+
+Conflict-free replicated data types
+
+https://crdt.tech/papers.html
+https://arxiv.org/abs/1806.10254
+
+Bazel
+
+https://brentley.dev/patching-bazel-external-dependencies/
+https://bazelbuild.github.io/rules_rust/
+
+Rust
+
+https://salsa.debian.org/rust-team/debcargo/
+https://wiki.debian.org/Teams/RustPackaging/Policy
+https://docs.rs/tui/latest/tui/
+
+LC in-browser initiative
+name "shared model"
+Speedcounting relies on strikebot to react fast, which leads to mistake strikes
+and requires rate limit circumvention.
+
+With "shared model" we enable strike bot to react much slower, while giving
+human feedback for speedcounting with even lower latency than strike bot can.
+
+Each in-browser view computes which updates to display as stricken, as
+prediction of the eventual thread state. This relies on the following aspects:
+* Views are eventually consistent on the set of updates in the viewport, their
+order, and which ones are stricken and deleted, without needing to refresh.
+At the most basic this will require transpo handling, but it also requires
+pulling liveupdate listings, critically when there is a websocket
+discontinuity, but at other times also: e.g. to confirm strikes.
+* Strike bot broadcasts thread tracking information periodically. The tracking
+information has to cover from the strike bot "frozen" timestamp (prior to this
+timestamp, the count tracking is reflected in reddit data and strike bot won't
+issue new strikes or deletes; can be far in the past and/or many updates ago
+if strike bot is overloaded) up until relatively recently (~25 updates ago),
+because views will have to load this far back in order to do local thread
+tracking. This may not have to include an entry for every update ID, and
+probably shouldn't due to traffic volume.
+
+We can actually do this WITHOUT P2P which is good because otherwise it won't
+happen.
+
+Strike bot HA: two or more instances can coordinate via reddit posts (comments)
+to elect a leader; the followers don't strike or delete but remain ready to
+promote if the leader disappears. No need to load-balance. Basically each
+instance posts "I'm alive" periodically to a separate area (editing a reddit
+comment); the leader instance is the lowest-numbered area whose timestamp isn't
+too stale.
+
+Strike bots do have to broadcast with this system so we need to be able to
+WebSocket connect to them. Hosting: bingus, strig (while exists), jcornell.net,
+occs?
+
+Ooh or we could use reddit for the broadcasts...hehehe.
+Basically we have some fixed amount of space (10000 code points: use base65536)
+and we want to encode...essentially a "valid count" list of update IDs.
+
+Handling count_better resets:
+- find the very next update that "can" be a valid count:
+  - has a number
+  - not stricken nor deleted
+  - it could be the same one as contains the reset command.
+- assume this one is valid; subsequent updates are (re)interpreted accordingly.
+- now find the latest valid count that *precedes* the reset. Try every
+  subsequent update, look for one that begins a chain of valid counts leading
+  up to the post-reset count.
+
+Algorithm for thread interpretation
+Start with no last_valid_ix, empty set of restart points, reset flag clear, ix = 0
+Loop start
+  If ix is out of bounds
+    If last_valid
+      Mark everything invalid after last_valid
+      Break
+    Failure
+  If update ix is not stricken
+    If update ix is reset command
+      Set reset flag
+    If update ix has number (n)
+      if update ix matches last_valid
+          Matching means last_valid was n-1 and the author is different
+        Set valid=true
+      Else if reset flag OR (no last_valid AND this update is blessed in the latest leader message)
+        Set ix to the earliest matching restart point, if any
+          Matching means the end of the restart point is n-1 with different author
+        Set valid=true
+      Else
+        If last_valid
+          Mark ix invalid
+        Is there a matching restart point?
+          Update the end of the earliest matching restart point to ix and remove other matching restart points
+        If no restart points matched
+          Create a new one with ix as start and end
+      If valid:
+        Clear the set of restart points
+        Clear the reset flag
+        Mark ix valid
+        Set last_valid to ix
+  Increment ix
diff --git a/sharedmodel/Cargo.toml b/sharedmodel/Cargo.toml
new file mode 100644 (file)
index 0000000..5113d4a
--- /dev/null
@@ -0,0 +1,20 @@
+[package]
+name = "sharedmodel"
+version = "0.1.0"
+authors = ["Andrea Cornell <anders@acorn.pw>"]
+edition = "2018"
+
+#[lib]
+#name = "sharedmodel"
+#path = "src/lib.rs"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+#[[bin]]
+#name = "sharedmodel"
+#path = "src/main.rs"
+
+[dependencies]
+uuid = "0.8"
+html5ever = "0.25"
+string_cache = "0.8"
diff --git a/sharedmodel/src/html_nodes.rs b/sharedmodel/src/html_nodes.rs
new file mode 100644 (file)
index 0000000..a585349
--- /dev/null
@@ -0,0 +1,16 @@
+use std::borrow::Borrow;
+
+#[derive(Debug)]
+pub enum HtmlNode<T, U> {
+       Text(T),
+       Element(U)
+}
+
+pub trait HtmlElement: Sized {
+       type Text1: AsRef<str>;
+       type Text2: AsRef<str>;
+       type ContentsIter: DoubleEndedIterator<Item=HtmlNode<Self::Text1, Self>>;
+       fn name(&self) -> &str;
+       fn contents(self) -> Self::ContentsIter;
+       fn text(&self) -> Self::Text2;
+}
diff --git a/sharedmodel/src/lib.rs b/sharedmodel/src/lib.rs
new file mode 100644 (file)
index 0000000..7a10431
--- /dev/null
@@ -0,0 +1,5 @@
+mod live_event_buffer;
+mod html_nodes;
+
+pub use live_event_buffer::LiveUpdateBuffer;
+pub use html_nodes::{HtmlNode, HtmlElement};
diff --git a/sharedmodel/src/live_event_buffer.rs b/sharedmodel/src/live_event_buffer.rs
new file mode 100644 (file)
index 0000000..dc4e36e
--- /dev/null
@@ -0,0 +1,137 @@
+use uuid::Uuid;
+use crate::html_nodes::HtmlElement;
+
+pub struct LiveUpdateMeta {
+       id: Uuid,
+       author: String,
+       stricken: bool
+}
+
+struct BufferedUpdate {
+       inner: LiveUpdateMeta,
+       counted_number: Option<i32>,
+       is_reset: bool,
+       bad_format: bool,
+       is_valid: bool
+}
+
+pub struct LiveUpdateBuffer {
+       capacity: usize,
+       buf: Vec<BufferedUpdate>
+}
+
+pub struct LeaderMessage {
+       blessed_ids: Vec<Uuid>
+}
+
+pub enum BufferEvent<T> {
+       Add { before: Option<Uuid>, meta: LiveUpdateMeta, body: T },
+       SetValid { id: Uuid, valid: bool },
+       Strike { id: Uuid },
+       Remove { id: Uuid }
+}
+
+impl LiveUpdateBuffer {
+       pub fn handle_updates<T: HtmlElement>(&mut self, u: &mut Vec<(LiveUpdateMeta, &T)>, ev: &mut Vec<BufferEvent<&T>>) {
+               
+       }
+
+       pub fn handle_strike(&mut self, id: Uuid, ev: &mut Vec<BufferEvent<()>>) {
+
+       }
+
+       pub fn handle_delete(&mut self, id: Uuid, ev: &mut Vec<BufferEvent<()>>) {
+
+       }
+
+       pub fn handle_leader_msg(&mut self, data: LeaderMessage, ev: &mut Vec<BufferEvent<()>>) {
+
+       }
+
+       fn can_follow(&self, ix1: usize, ix2: usize) -> bool {
+               let upd1 = self.buf.get(ix1).unwrap();
+               let upd2 = self.buf.get(ix2).unwrap();
+               if upd1.inner.stricken || upd2.inner.stricken || upd1.inner.author.eq(&upd2.inner.author) {
+                       return false;
+               }
+               match (upd1.counted_number, upd2.counted_number) {
+                       (Some(n1), Some(n2)) => n2 == n1 + 1,
+                       _ => false
+               }
+       }
+
+       fn mark_valid(&mut self, ix: usize, valid: bool, ev: &mut Vec<BufferEvent<()>>) {
+               let upd = self.buf.get_mut(ix).unwrap();
+               upd.is_valid = valid;
+               ev.push(BufferEvent::SetValid { id: upd.inner.id.clone(), valid })
+       }
+
+       fn interpret_thread(&mut self, ev: &mut Vec<BufferEvent<()>>) {
+               let mut last_valid: Option<usize> = None;
+               let mut restart_points = Vec::<(usize, usize)>::new();
+               let mut reset_pending = false;
+               let mut ix = 0;
+               loop {
+                       if ix >= self.buf.len() {
+                               if let Some(last_valid) = last_valid {
+                                       for ix in last_valid..ix {
+                                               self.mark_valid(ix, false, ev);
+                                       }
+                               } // else: failure
+                               break;
+                       }
+                       let upd = self.buf.get(ix).unwrap();
+                       if !upd.inner.stricken {
+                               if upd.is_reset {
+                                       reset_pending = true;
+                               }
+                               if let Some(n) = upd.counted_number {
+                                       let is_valid = last_valid.map_or(self.check_blessed(ix), |last_valid| self.can_follow(last_valid, ix)) || reset_pending;
+                                       if reset_pending {
+                                               if let Some(restart_from) = restart_points.iter().find_map(|(restart_from, restart_to)| self.can_follow(*restart_to, ix).then_some(restart_from) ) {
+//                                             if let Some(restart_from) = restart_points.iter().find_map(|(restart_from, restart_to)| /*self.can_follow(*restart_to, ix).then_some(restart_from)*/ if self.can_follow(*restart_to, ix) {Some(restart_from)} else {None}) {
+                                                       ix = *restart_from;
+                                               }
+                                       }
+                                       if is_valid {
+                                               restart_points.clear();
+                                               reset_pending = false;
+                                               self.mark_valid(ix, true, ev);
+                                               last_valid = Some(ix);
+                                       }
+                                       else {
+                                               if last_valid.is_some() {
+                                                       self.mark_valid(ix, false, ev);
+                                               }
+                                               let mut cur = 0;
+                                               let mut matching = None;
+                                               restart_points.retain(|(restart_from, restart_to)| {
+                                                       let cur0 = cur;
+                                                       cur += 1;
+                                                       if !self.can_follow(*restart_to, ix) {
+                                                               return true;
+                                                       }
+                                                       if matching.is_none() {
+                                                               matching = Some(cur0);
+                                                               return true;
+                                                       }
+                                                       false
+                                               });
+                                               match matching {
+                                                       Some(ix) => {
+                                                               let o: &mut (usize, usize) = restart_points.get_mut(ix).unwrap();
+                                                               o.1 = ix },
+                                                       None => restart_points.push((ix, ix))
+                                               }
+                                       }
+                               }
+                       }
+                       ix += 1;
+               }
+       }
+
+       fn check_blessed(&self, ix: usize) -> bool {
+               // Check the latest leader message to see if the given update is blessed
+               false
+       }
+}
diff --git a/sharedmodel/src/main.rs b/sharedmodel/src/main.rs
new file mode 100644 (file)
index 0000000..37a8bc4
--- /dev/null
@@ -0,0 +1,205 @@
+use std::cell::{Cell, RefCell};
+use std::rc::{Rc, Weak};
+use std::borrow::Cow;
+
+use html5ever::tree_builder::{TreeSink, ElementFlags};
+use html5ever::{QualName, ExpandedName, LocalNameStaticSet, Attribute};
+use html5ever::tendril::{StrTendril, TendrilSink};
+use html5ever::interface::{NodeOrText, QuirksMode};
+use html5ever::driver::ParseOpts;
+use string_cache::Atom;
+
+use sharedmodel::{HtmlNode, HtmlElement};
+
+fn main() {
+    let mut x = html5ever::parse_fragment(RcHtmlElement(Rc::new(HtmlElementInner::root())), ParseOpts::default(), QualName::new(None, Atom::from("http://www.w3.org/1999/xhtml"), Atom::from("body")), Vec::new());
+       //let html = "<p>This is <em>a <a href=\"#\">paragraph</a></em>. There are</p><ul><li>nested</li><li>tags, <strong>especially in <em>here</em></strong></li></ul>";
+       //let html = "<p>A paragraph</p>";
+       let html = "<section><p>Can you handle this</p><table><tr><th>a</th><th>b</th></tr><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr></table></section>";
+       x.process(StrTendril::from(html));
+       let tr = x.finish();
+       println!("{:?}", tr);
+       println!("{}", tr.text())
+}
+
+type RcNode<'a> = HtmlNode<StrTendril, RcHtmlElement<'a>>;
+
+// #[derive(Debug)]
+struct HtmlElementInner<'a> {
+       name: QualName,
+       parent: Cell<Weak<HtmlElementInner<'a>>>,
+       contents: RefCell<Vec<RcNode<'a>>>
+}
+
+impl HtmlElementInner<'_> {
+       fn root() -> Self {
+               let name = QualName::new(None, Atom::from(""), Atom::from(""));
+               let parent = Cell::new(Weak::new());
+               let contents = RefCell::new(Vec::new());
+               HtmlElementInner { name, parent, contents }
+       }
+}
+
+//#[derive(Clone, Debug)]
+#[derive(Clone)]
+struct RcHtmlElement<'a>(Rc<HtmlElementInner<'a>>);
+
+fn nodeortext_to_htmlnode<'a>(v: NodeOrText<RcHtmlElement<'a>>) -> RcNode<'a> {
+       match v {
+               NodeOrText::AppendNode(h) => HtmlNode::Element(h),
+               NodeOrText::AppendText(s) => HtmlNode::Text(s)
+       }
+}
+
+fn node_eq_elem<'a>(node: &RcNode<'a>, elem: &RcHtmlElement<'a>) -> bool {
+       match node {
+               HtmlNode::Element(elem1) if Rc::ptr_eq(&elem.0, &elem1.0) => true,
+               _ => false
+       }
+}
+
+impl<'a> TreeSink for RcHtmlElement<'a> {
+       type Handle = Self;
+       type Output = UniqHtmlElement;
+       fn finish(self) -> UniqHtmlElement {
+               // let HtmlElementInner { name, parent, contents } = Rc::try_unwrap(self.0).map_err(|_| ()).expect("no refs left around");
+               let HtmlElementInner { ref name, ref contents, .. } = *self.0;
+               let name = name.local.clone();
+               let contents = contents.borrow_mut().drain(..).map(|node| {
+                       match node {
+                               HtmlNode::Text(s) => HtmlNode::Text(s),
+                               HtmlNode::Element(e) => HtmlNode::Element(e.finish())
+                       }
+               }).collect();
+               UniqHtmlElement { name, contents }
+       }
+
+       fn parse_error(&mut self, msg: Cow<'static, str>) {
+               println!("{}", msg);
+               panic!("how do parse??");
+       }
+
+       fn get_document(&mut self) -> Self::Handle {
+               self.clone()
+       }
+
+       fn elem_name<'b>(&'b self, target: &'b Self::Handle) -> ExpandedName<'b> {
+               target.0.name.expanded()
+       }
+
+       fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>, flags: ElementFlags) -> Self::Handle {
+               assert_eq!(flags.template, false);
+               assert_eq!(flags.mathml_annotation_xml_integration_point, false);
+               let contents = RefCell::new(Vec::new());
+               let parent = Cell::new(Weak::new());
+               // handily, we never use attribute data. Toss it
+               RcHtmlElement(Rc::new(HtmlElementInner { name, parent, contents }))
+       }
+
+       fn create_comment(&mut self, text: StrTendril) -> Self::Handle {
+               panic!("HTML parser encountered a comment");
+       }
+
+       fn create_pi(&mut self, target: StrTendril, data: StrTendril) -> Self::Handle {
+               panic!("HTML parser encountered a processing instruction");
+       }
+
+       fn append(&mut self, parent: &Self::Handle, child: NodeOrText<Self::Handle>) {
+               let node = nodeortext_to_htmlnode(child);
+               if let HtmlNode::Element(ref elem) = node {
+                       elem.0.parent.set(Rc::downgrade(&parent.0));
+               }
+               parent.0.contents.borrow_mut().push(node);
+       }
+
+       fn append_based_on_parent_node(&mut self, element: &Self::Handle, prev_element: &Self::Handle, child: NodeOrText<Self::Handle>) {
+               panic!("Not clear what this is for or if it needs to be implemented");
+       }
+
+       fn append_doctype_to_document(&mut self, name: StrTendril, public_id: StrTendril, system_id: StrTendril) {
+               panic!("This can likely be a no-op, just not sure if it gets called for these fragments");
+       }
+
+       fn get_template_contents(&mut self, target: &Self::Handle) -> Self::Handle {
+               panic!("HTML parser encountered a <template> element")
+       }
+
+       fn same_node(&self, x: &Self::Handle, y: &Self::Handle) -> bool {
+               Rc::ptr_eq(&x.0, &y.0)
+       }
+
+       fn set_quirks_mode(&mut self, mode: QuirksMode) {}
+
+       fn append_before_sibling(&mut self, sibling: &Self::Handle, new_node: NodeOrText<Self::Handle>) {
+               let parent_weak = sibling.0.parent.take();
+               let parent = parent_weak.upgrade().expect("parent is not dropped");
+               let mut children = parent.contents.borrow_mut();
+               sibling.0.parent.set(parent_weak);
+               let pos = children.iter().position(|node| node_eq_elem(node, sibling)).expect("sibling is child of parent");
+               children.insert(pos, nodeortext_to_htmlnode(new_node));
+       }
+
+       fn add_attrs_if_missing(&mut self, target: &Self::Handle, attrs: Vec<Attribute>) {}
+
+       fn remove_from_parent(&mut self, target: &Self::Handle) {
+               let parent = target.0.parent.replace(Weak::new()).upgrade().expect("target has parent");
+               let mut children = parent.contents.borrow_mut();
+               let pos = children.iter().position(|node| node_eq_elem(node, target)).expect("sibling is child of parent");
+               children.remove(pos);
+       }
+
+       fn reparent_children(&mut self, node: &Self::Handle, new_parent: &Self::Handle) {
+               new_parent.0.contents.borrow_mut().extend(node.0.contents.borrow_mut().drain(..));
+       }
+}
+
+
+#[derive(Debug)]
+struct UniqHtmlElement {
+       name: Atom<LocalNameStaticSet>,
+       contents: Vec<HtmlNode<StrTendril, UniqHtmlElement>>
+}
+
+/*fn node_text(node: &HtmlNode<StrTendril, UniqHtmlElement>) -> Cow<str> {
+       match node {
+               HtmlNode::Text(s) => Cow::Borrowed(s.as_ref()),
+               HtmlNode::Element(e) => e.text()
+       }
+}*/
+
+fn element_text_inner(buf: &mut Option<StrTendril>, elem: &UniqHtmlElement) {
+       for node in &elem.contents {
+               match node {
+                       HtmlNode::Text(s) => match buf {
+                               None => { *buf = Some(s.clone()); },
+                               Some(tendril) => tendril.push_tendril(&s)
+                       },
+                       HtmlNode::Element(e) => element_text_inner(buf, &e)
+               }
+       }
+}
+
+impl HtmlElement for UniqHtmlElement {
+       type Text1 = StrTendril;
+       type Text2 = StrTendril;
+       type ContentsIter = <Vec<HtmlNode<Self::Text1, Self>> as IntoIterator>::IntoIter;
+       fn name(&self) -> &str {
+               &*self.name
+       }
+       fn contents(self) -> Self::ContentsIter {
+               self.contents.into_iter()
+       }
+       fn text(&self) -> StrTendril {
+               let mut result = None;
+               element_text_inner(&mut result, self);
+               result.unwrap_or(StrTendril::new())
+/*             let mut init = self.contents.get(0).map(node_text).unwrap_or(Cow::Borrowed(""));
+               for node in &self.contents[1..] {
+                       let text = node_text(node);
+                       if text.len() > 0 {
+                               init.to_mut().push_str(&text);
+                       }
+               }
+               init*/
+       }
+}
diff --git a/sharedmodel_wasm/Cargo.toml b/sharedmodel_wasm/Cargo.toml
new file mode 100644 (file)
index 0000000..29cdeb3
--- /dev/null
@@ -0,0 +1,15 @@
+[package]
+name = "sharedmodel_wasm"
+version = "0.1.0"
+authors = ["Andrea Cornell <anders@acorn.pw>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+js-sys = "0.3"
+web-sys = { version = "0.3", features = ["HtmlElement", "NodeList"] }
+wasm-bindgen = "0.2"
+
+[dependencies.sharedmodel]
+path = "../sharedmodel"
diff --git a/sharedmodel_wasm/src/lib.rs b/sharedmodel_wasm/src/lib.rs
new file mode 100644 (file)
index 0000000..1c06b21
--- /dev/null
@@ -0,0 +1,107 @@
+use sharedmodel::{HtmlElement, HtmlNode};
+
+struct JsDomHtmlElement {
+       tag_name: String,
+       elem: web_sys::HtmlElement
+}
+
+impl JsDomHtmlElement {
+       fn new(elem: web_sys::HtmlElement) -> Self {
+               let tag_name = elem.tag_name();
+               Self { tag_name, elem }
+       }
+}
+
+impl HtmlElement for JsDomHtmlElement {
+       type Text1 = String;
+       type Text2 = String;
+//     type Node = HtmlNode<Self::Text1, Self>;
+       type ContentsIter = std::iter::FilterMap<JsDomContentsIter, fn(web_sys::Node) -> Option<HtmlNode<String, JsDomHtmlElement>>>;
+       fn name(&self) -> &str {
+               &self.tag_name
+/*             static names = ["em", "strong", "del", "span", "sup", "code", "a", "th", "td",
+                               "ul", "ol", "table", "thead", "tbody", "li", "p", "div", "blockquote",
+                               "h1", "h2", "h3", "h4", "h5", "h6"];
+               static name_jsmap = Lazy::new(|| {
+                       let v = js_sys::Map::new();
+                       for (name, ix) in names.enumerate() {
+                               v.add(JsValue::from_str(name), JsValue::from_f64(ix));
+                       }
+                       v
+               });*/
+       }
+       fn contents(self) -> Self::ContentsIter {
+               JsDomContentsIter::new(self.elem).filter_map(|node| {
+                       use web_sys::Node;
+                       use wasm_bindgen::JsCast;
+                       match node.node_type() {
+                               Node::ELEMENT_NODE => Some(HtmlNode::Element(JsDomHtmlElement::new(node.dyn_into().expect("Element node instanceof HtmlElement")))),
+                               Node::TEXT_NODE => Some(HtmlNode::Text(node.text_content().unwrap_or(String::new()))),
+                               _ => None
+                       }
+               })
+       }
+       fn text(&self) -> String {
+               self.elem.text_content().unwrap_or(String::new())
+       }
+}
+
+struct JsDomContentsIter {
+       parent: web_sys::HtmlElement,
+       head: Option<web_sys::Node>,
+       tail: Option<web_sys::Node>,
+}
+
+//struct JsDomContentsIter(Option<(web_sys::Node, web_sys::Node)>);
+
+impl JsDomContentsIter {
+       fn new(elem: web_sys::HtmlElement) -> Self {
+               Self { parent: elem, head: None, tail: None }
+       }
+}
+
+impl Iterator for JsDomContentsIter {
+       type Item = web_sys::Node; // HtmlNode<String, JsDomHtmlElement>;
+       fn next(&mut self) -> Option<Self::Item> {
+               let next = self.head.as_ref().map_or_else(
+                       || self.parent.first_child(),
+                       |head| head.next_sibling()
+               );
+               if let Some(next) = next {
+                       if self.tail.as_ref().filter(|tail| next.eq(tail)).is_none() {
+                               self.head = Some(next.clone());
+                               return Some(next);
+                       }
+               }
+               None
+       } /*
+               self.head = self.head.or_else(
+                       || self.parent.first_child(),
+                       |head| head
+               );
+               self.head.filter(|head| !self.tail.and_then(|tail| head.eq(tail)))
+       }
+               if self.ended {
+                       None
+               }
+               else {
+                       
+                       self.ended = true;
+               }
+               // TODO need to skip over attribute nodes
+               let ret = self.nodelist.item(self.index).map(|node| {
+                       use web_sys::Node;
+                       use wasm_bindgen::JsCast;
+                       match node.node_type() {
+                               Node::ELEMENT_NODE => HtmlNode::Element(JsDomHtmlElement::new(node.dyn_into().expect("Element node instanceof HtmlElement"))),
+                               _ => HtmlNode::Text(node.text_content().unwrap_or(String::new())) // Node::TEXT_NODE
+                       }
+               });
+               self.index += 1;
+               ret
+       } */
+}
+
+impl DoubleEndedIterator for JsDomContentsIter {
+       fn next_back(&mut self) -> Option<Self::Item> { todo!() }
+}