diff --git a/.cargo/config.toml b/.cargo/config.toml index 0b17254..4768414 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,2 @@ [env] CARGO_WORKSPACE_DIR = { value = "", relative = true } - -[target.x86_64-unknown-linux-gnu] -rustflags = ["-C", "target-cpu=native"] diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 26f6d00..4e465e5 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -4,9 +4,9 @@ inputs: components: description: "Cargo components" required: false - target: + targets: description: "Cargo target" - required: true + required: false toolchain: description: "Rustup toolchain" required: false @@ -19,6 +19,6 @@ runs: uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ inputs.toolchain }} - targets: ${{ inputs.target }} + targets: ${{ inputs.targets }} components: ${{ inputs.components }} - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 19005c9..141c686 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -24,8 +24,8 @@ jobs: with: ref: main - - name: Install Rust - uses: dtolnay/rust-toolchain@stable + - name: Setup Rust + uses: ./.github/actions/setup-rust with: toolchain: stable @@ -66,6 +66,16 @@ jobs: - benchmark steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + toolchain: stable + - name: Install critcmp uses: taiki-e/install-action@v2 with: @@ -80,11 +90,7 @@ jobs: - name: Linux | Compare benchmark results shell: bash run: | - echo "## Benchmark Results" >> summary.md - echo "### Linux" >> summary.md - echo "\`\`\`" >> summary.md - critcmp main pr >> summary.md - echo "\`\`\`" >> summary.md + critcmp main pr | cargo run -p y-octo-utils --bin bench_result_render --features bench -- Linux >> summary.md echo "" >> summary.md - name: Linux | Cleanup benchmark results @@ -99,11 +105,7 @@ jobs: - name: Windows | Compare benchmark results shell: bash run: | - echo "### Windows" >> summary.md - echo "\`\`\`" >> summary.md - critcmp main pr >> summary.md - echo "\`\`\`" >> summary.md - echo "" >> summary.md + critcmp main pr | cargo run -p y-octo-utils --bin bench_result_render --features bench -- Windows >> summary.md cat summary.md > $GITHUB_STEP_SUMMARY - name: Find Comment diff --git a/.github/workflows/y-octo.yml b/.github/workflows/y-octo.yml index a1b9722..dc890c1 100644 --- a/.github/workflows/y-octo.yml +++ b/.github/workflows/y-octo.yml @@ -1,4 +1,4 @@ -name: Build Y-Octo +name: Lint & Test & Fuzzing & Miri & Loom on: workflow_dispatch: @@ -7,6 +7,9 @@ on: pull_request: branches: [main] +env: + nightly: nightly-2023-08-19 + # Cancels all previous workflow runs for pull requests that have not completed. # See https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: @@ -50,8 +53,8 @@ jobs: - name: Build & Check run: | - cargo vendor > .cargo/config - cargo clippy --all-features --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + cargo vendor > .cargo/config.toml + cargo clippy --all-features --message-format=json -- -D warnings | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links" cargo doc --workspace --all-features --no-deps env: CARGO_TERM_COLOR: always @@ -88,53 +91,70 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - name: Collect coverage data - run: cargo llvm-cov nextest --lcov --output-path lcov.info + run: cargo llvm-cov nextest --all-targets --lcov --output-path lcov.info - name: Upload coverage data to codecov uses: codecov/codecov-action@v3 with: name: tests files: lcov.info - memory_check: - name: memory check + loom: + name: loom thread test runs-on: ubuntu-latest continue-on-error: true env: - RUSTFLAGS: -D warnings -Zsanitizer=address - ASAN_OPTIONS: detect_leaks=1 + RUSTFLAGS: --cfg loom + RUST_BACKTRACE: full CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v3 - name: Setup Rust uses: ./.github/actions/setup-rust - with: - toolchain: nightly-2023-08-19 + - name: Install latest nextest release + uses: taiki-e/install-action@nextest - - name: Memory Check + - name: Loom Thread Test run: | - rustup component add rust-src --toolchain nightly-2023-08-19 - cargo +nightly-2023-08-19 test -Zbuild-std --target x86_64-unknown-linux-gnu -p y-octo --lib + cargo nextest run -p y-octo --lib - loom: - name: loom thread test + fuzzing: + name: fuzzing runs-on: ubuntu-latest continue-on-error: true env: - RUSTFLAGS: --cfg loom - RUST_BACKTRACE: full + RUSTFLAGS: -D warnings CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v3 - name: Setup Rust uses: ./.github/actions/setup-rust - - name: Install latest nextest release - uses: taiki-e/install-action@nextest + with: + toolchain: ${{ env.nightly }} - - name: Loom Thread Test + - name: fuzzing + working-directory: ./y-octo run: | - cargo nextest run -p y-octo --lib + cargo install cargo-fuzz + cargo +${{ env.nightly }} fuzz run apply_update -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run codec_doc_any_struct -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run codec_doc_any -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run decode_bytes -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run i32_decode -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run i32_encode -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run ins_del_text -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run sync_message -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run u64_decode -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run u64_encode -- -max_total_time=30 + cargo +${{ env.nightly }} fuzz run apply_update -- -max_total_time=30 + + - name: upload fuzz artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: fuzz-artifact + path: ./y-octo/fuzz/artifacts/**/* miri: name: miri code check @@ -150,21 +170,22 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly-2023-08-19 + toolchain: ${{ env.nightly }} components: miri - name: Install latest nextest release uses: taiki-e/install-action@nextest - name: Miri Code Check run: | - cargo +nightly-2023-08-19 miri nextest run -p y-octo -j2 + cargo +${{ env.nightly }} miri nextest run -p y-octo -j2 - fuzzing: - name: fuzzing + asan: + name: Address sanitizer runs-on: ubuntu-latest continue-on-error: true env: - RUSTFLAGS: -D warnings + RUSTFLAGS: -D warnings -Zsanitizer=address + ASAN_OPTIONS: detect_leaks=1 CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v3 @@ -172,27 +193,9 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly-2023-08-19 + toolchain: ${{ env.nightly }} - - name: fuzzing - working-directory: ./y-octo + - name: Memory Check run: | - cargo install cargo-fuzz - cargo +nightly-2023-08-19 fuzz run apply_update -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run codec_doc_any_struct -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run codec_doc_any -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run decode_bytes -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run i32_decode -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run i32_encode -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run ins_del_text -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run sync_message -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run u64_decode -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run u64_encode -- -max_total_time=30 - cargo +nightly-2023-08-19 fuzz run apply_update -- -max_total_time=30 - - - name: upload fuzz artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v3 - with: - name: fuzz-artifact - path: ./y-octo/fuzz/artifacts/**/* + rustup component add rust-src --toolchain ${{ env.nightly }} + cargo +${{ env.nightly }} test -Zbuild-std --target x86_64-unknown-linux-gnu -p y-octo --lib diff --git a/README.md b/README.md index c3cbc2c..33e702f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,82 @@ # Y-Octo +[![test](https://github.com/toeverything/y-octo/actions/workflows/y-octo.yml/badge.svg)](https://github.com/toeverything/y-octo/actions/workflows/y-octo.yml) +[![docs]](https://docs.rs/y-octo/latest/y_octo) +[![crates]](https://crates.io/crates/y-octo) [![codecov]](https://codecov.io/gh/toeverything/y-octo) Y-Octo is a high-performance CRDT implementation compatible with [yjs]. -Y-Octo aims to provide a thread-safe, high-performance CRDT implementation on multiple platforms and offers binary compatibility and interoperability with [yjs]. +### Introduction + +Y-Octo is a tiny, ultra-fast CRDT collaboration library built for all major platforms. Developers can use Y-Octo as the [Single source of truth](https://en.wikipedia.org/wiki/Single_source_of_truth) for their application state, naturally turning the application into a [local-first](https://www.inkandswitch.com/local-first/) collaborative app. + +Y-Octo also has interoperability and binary compatibility with [yjs]. Developers can use [yjs] to develop local-first web applications and collaborate with Y-Octo in native apps alongside web apps. + +### Features + +- βœ… Collaborative Text + - βœ… Read and write styled Unicode compatible data. + - 🚧 Add, modify and delete text styles. + - 🚧 Embedded JS data types and collaborative types. + - βœ… Collaborative types of thread-safe. +- Collaborative Array + - βœ… Add, modify, and delete basic JS data types. + - βœ… Recursively add, modify, and delete collaborative types. + - βœ… Collaborative types of thread-safe. + - 🚧 Recursive event subscription +- Collaborative Map + - βœ… Add, modify, and delete basic JS data types. + - βœ… Recursively add, modify, and delete collaborative types. + - βœ… Collaborative types of thread-safe. + - 🚧 Recursive event subscription +- 🚧 Collaborative Xml (Fragment / Element) +- βœ… Collaborative Doc Container + - βœ… YATA CRDT state apply/diff compatible with [yjs] + - βœ… State sync of thread-safe. + - βœ… Store all collaborative types and JS data types + - βœ… Update event subscription. + - 🚧 Sub Document. +- βœ… Yjs binary encoding + - βœ… Awareness encoding. + - βœ… Primitive type encoding. + - βœ… Sync Protocol encoding. + - βœ… Yjs update v1 encoding. + - 🚧 Yjs update v2 encoding. + +### Testing & Linting + +Put everything to the test! We've established various test suites, but we're continually striving to enhance our coverage: + +- Rust Tests +- Node Tests +- Smoke Tests +- eslint, clippy + +### Related projects + +- [OctoBase]: The open-source embedded database based on Y-Octo. +- [yjs]: Shared data types for building collaborative software in web. + +## Maintainers + +- [DarkSky](https://github.com/darkskygit) +- [liuyi](https://github.com/forehalo) +- [X1a0t](https://github.com/thorseraq) +- [LongYinan](https://github.com/Brooooooklyn) + +## License + +Y-Octo are [MIT licensed]. [codecov]: https://codecov.io/gh/toeverything/y-octo/graph/badge.svg?token=9AQY5Q1BYH +[crates]: https://img.shields.io/crates/v/y-octo.svg +[docs]: https://img.shields.io/docsrs/y-octo.svg +[test]: https://github.com/toeverything/y-octo/actions/workflows/y-octo.yml/badge.svg [yjs]: https://github.com/yjs/yjs +[Address Sanitizer]: https://github.com/toeverything/y-octo/actions/workflows/y-octo-asan.yml/badge.svg +[Memory Leak Detect]: https://github.com/toeverything/y-octo/actions/workflows/y-octo-memory-test.yml/badge.svg +[OctoBase]: https://github.com/toeverything/octobase +[BlockSuite]: https://github.com/toeverything/blocksuite +[AFFiNE]: https://github.com/toeverything/affine +[MIT licensed]: ./LICENSE diff --git a/package.json b/package.json index bf1bbd7..5837b91 100644 --- a/package.json +++ b/package.json @@ -39,5 +39,47 @@ "*.rs": [ "cargo +nightly-2023-08-19 fmt --" ] + }, + "resolutions": { + "array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest", + "arraybuffer.prototype.slice": "npm:@nolyfill/arraybuffer.prototype.slice@latest", + "available-typed-arrays": "npm:@nolyfill/available-typed-arrays@latest", + "define-properties": "npm:@nolyfill/define-properties@latest", + "es-set-tostringtag": "npm:@nolyfill/es-set-tostringtag@latest", + "function-bind": "npm:@nolyfill/function-bind@latest", + "function.prototype.name": "npm:@nolyfill/function.prototype.name@latest", + "get-symbol-description": "npm:@nolyfill/get-symbol-description@latest", + "globalthis": "npm:@nolyfill/globalthis@latest", + "gopd": "npm:@nolyfill/gopd@latest", + "has": "npm:@nolyfill/has@latest", + "has-property-descriptors": "npm:@nolyfill/has-property-descriptors@latest", + "has-proto": "npm:@nolyfill/has-proto@latest", + "has-symbols": "npm:@nolyfill/has-symbols@latest", + "has-tostringtag": "npm:@nolyfill/has-tostringtag@latest", + "internal-slot": "npm:@nolyfill/internal-slot@latest", + "is-array-buffer": "npm:@nolyfill/is-array-buffer@latest", + "is-date-object": "npm:@nolyfill/is-date-object@latest", + "is-regex": "npm:@nolyfill/is-regex@latest", + "is-shared-array-buffer": "npm:@nolyfill/is-shared-array-buffer@latest", + "is-string": "npm:@nolyfill/is-string@latest", + "is-symbol": "npm:@nolyfill/is-symbol@latest", + "is-weakref": "npm:@nolyfill/is-weakref@latest", + "object-keys": "npm:@nolyfill/object-keys@latest", + "object.assign": "npm:@nolyfill/object.assign@latest", + "regexp.prototype.flags": "npm:@nolyfill/regexp.prototype.flags@latest", + "safe-array-concat": "npm:@nolyfill/safe-array-concat@latest", + "safe-regex-test": "npm:@nolyfill/safe-regex-test@latest", + "side-channel": "npm:@nolyfill/side-channel@latest", + "string.prototype.padend": "npm:@nolyfill/string.prototype.padend@latest", + "string.prototype.trim": "npm:@nolyfill/string.prototype.trim@latest", + "string.prototype.trimend": "npm:@nolyfill/string.prototype.trimend@latest", + "string.prototype.trimstart": "npm:@nolyfill/string.prototype.trimstart@latest", + "typed-array-buffer": "npm:@nolyfill/typed-array-buffer@latest", + "typed-array-byte-length": "npm:@nolyfill/typed-array-byte-length@latest", + "typed-array-byte-offset": "npm:@nolyfill/typed-array-byte-offset@latest", + "typed-array-length": "npm:@nolyfill/typed-array-length@latest", + "unbox-primitive": "npm:@nolyfill/unbox-primitive@latest", + "which-boxed-primitive": "npm:@nolyfill/which-boxed-primitive@latest", + "which-typed-array": "npm:@nolyfill/which-typed-array@latest" } } diff --git a/y-octo-utils/Cargo.toml b/y-octo-utils/Cargo.toml index 928a185..b082fc7 100644 --- a/y-octo-utils/Cargo.toml +++ b/y-octo-utils/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["x1a0t <405028157@qq.com>"] +authors = ["x1a0t <405028157@qq.com>", "DarkSky "] edition = "2021" license = "MIT" name = "y-octo-utils" @@ -7,8 +7,17 @@ version = "0.0.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +bench = ["regex"] +fuzz = ["arbitrary", "phf", "y-octo", "yrs"] + [dependencies] -arbitrary = { version = "1.3.0", features = ["derive"] } -phf = { version = "0.11", features = ["macros"] } -y-octo = { workspace = true } -yrs = "=0.16.5" +arbitrary = { version = "1.3.0", features = ["derive"], optional = true } +phf = { version = "0.11", features = ["macros"], optional = true } +regex = { version = "1.5", optional = true } +y-octo = { workspace = true, optional = true } +yrs = { version = "=0.16.5", optional = true } + +[[bin]] +name = "bench_result_render" +path = "bin/bench_result_render.rs" diff --git a/y-octo-utils/bin/bench_result_render.rs b/y-octo-utils/bin/bench_result_render.rs new file mode 100644 index 0000000..b47f1e1 --- /dev/null +++ b/y-octo-utils/bin/bench_result_render.rs @@ -0,0 +1,121 @@ +use std::{ + collections::HashMap, + io::{self, BufRead}, +}; + +fn process_duration(duration: &str) -> Option<(f64, f64)> { + let dur_split: Vec = duration.split('Β±').map(String::from).collect(); + if dur_split.len() != 2 { + return None; + } + let units = dur_split[1] + .chars() + .skip_while(|c| c.is_ascii_digit()) + .collect::(); + let dur_secs = dur_split[0].parse::().ok()?; + let error_secs = dur_split[1] + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse::() + .ok()?; + Some(( + convert_dur_to_seconds(dur_secs, &units), + convert_dur_to_seconds(error_secs, &units), + )) +} + +fn convert_dur_to_seconds(dur: f64, units: &str) -> f64 { + let factors: HashMap<_, _> = [ + ("s", 1.0), + ("ms", 1.0 / 1000.0), + ("Β΅s", 1.0 / 1_000_000.0), + ("ns", 1.0 / 1_000_000_000.0), + ] + .iter() + .cloned() + .collect(); + dur * factors.get(units).unwrap_or(&1.0) +} + +fn is_significant(changes_dur: f64, changes_err: f64, base_dur: f64, base_err: f64) -> bool { + if changes_dur < base_dur { + changes_dur + changes_err < base_dur || base_dur - base_err > changes_dur + } else { + changes_dur - changes_err > base_dur || base_dur + base_err < changes_dur + } +} + +fn convert_to_markdown() -> impl Iterator { + #[cfg(feature = "bench")] + let re = regex::Regex::new(r"\s{2,}").unwrap(); + io::stdin() + .lock() + .lines() + .skip(2) + .flat_map(move |row| { + if let Ok(_row) = row { + let columns = { + #[cfg(feature = "bench")] + { + re.split(&_row).collect::>() + } + #[cfg(not(feature = "bench"))] + Vec::<&str>::new() + }; + let name = columns.first()?; + let base_duration = columns.get(2)?; + let changes_duration = columns.get(5)?; + Some(( + name.to_string(), + base_duration.to_string(), + changes_duration.to_string(), + )) + } else { + None + } + }) + .flat_map(|(name, base_duration, changes_duration)| { + let mut difference = "N/A".to_string(); + let base_undefined = base_duration == "?"; + let changes_undefined = changes_duration == "?"; + + if !base_undefined && !changes_undefined { + let (base_dur_secs, base_err_secs) = process_duration(&base_duration)?; + let (changes_dur_secs, changes_err_secs) = process_duration(&changes_duration)?; + + let diff = -(1.0 - changes_dur_secs / base_dur_secs) * 100.0; + difference = format!("{:+.2}%", diff); + + if is_significant(changes_dur_secs, changes_err_secs, base_dur_secs, base_err_secs) { + difference = format!("**{}**", difference); + } + } + + Some(format!( + "| {} | {} | {} | {} |", + name.replace('|', "\\|"), + if base_undefined { "N/A" } else { &base_duration }, + if changes_undefined { "N/A" } else { &changes_duration }, + difference + )) + }) +} + +fn main() { + let platform = std::env::args().nth(1).expect("Missing platform argument"); + + let headers = vec![ + format!("## Benchmark for {}", platform), + "
".to_string(), + " Click to view benchmark".to_string(), + "".to_string(), + "| Test | Base | PR | % |".to_string(), + "| --- | --- | --- | --- |".to_string(), + ]; + + for line in headers.into_iter().chain(convert_to_markdown()) { + println!("{}", line); + } + println!("
"); +} diff --git a/y-octo-utils/src/lib.rs b/y-octo-utils/src/lib.rs index 552f05e..5474319 100644 --- a/y-octo-utils/src/lib.rs +++ b/y-octo-utils/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "fuzz")] pub mod doc_operation; +#[cfg(feature = "fuzz")] pub use doc_operation::*; diff --git a/y-octo/fuzz/Cargo.toml b/y-octo/fuzz/Cargo.toml index 2ec36f7..e117ae3 100644 --- a/y-octo/fuzz/Cargo.toml +++ b/y-octo/fuzz/Cargo.toml @@ -14,7 +14,7 @@ rand = "0.8" rand_chacha = "0.3" yrs = "=0.16.5" -y-octo-utils = { path = "../../y-octo-utils" } +y-octo-utils = { path = "../../y-octo-utils", features = ["fuzz"] } [dependencies.y-octo] path = ".." diff --git a/y-octo/src/doc/codec/any.rs b/y-octo/src/doc/codec/any.rs index 1ab9dc1..ee2e09c 100644 --- a/y-octo/src/doc/codec/any.rs +++ b/y-octo/src/doc/codec/any.rs @@ -253,6 +253,51 @@ impl From for Any { } } +impl TryFrom for String { + type Error = JwstCodecError; + + fn try_from(value: Any) -> Result { + match value { + Any::String(s) => Ok(s), + _ => Err(JwstCodecError::UnexpectedType("String")), + } + } +} + +impl TryFrom for HashMap { + type Error = JwstCodecError; + + fn try_from(value: Any) -> Result { + match value { + Any::Object(map) => Ok(map), + _ => Err(JwstCodecError::UnexpectedType("Object")), + } + } +} + +impl TryFrom for Vec { + type Error = JwstCodecError; + + fn try_from(value: Any) -> Result { + match value { + Any::Array(vec) => Ok(vec), + _ => Err(JwstCodecError::UnexpectedType("Array")), + } + } +} + +impl TryFrom for bool { + type Error = JwstCodecError; + + fn try_from(value: Any) -> Result { + match value { + Any::True => Ok(true), + Any::False => Ok(false), + _ => Err(JwstCodecError::UnexpectedType("Boolean")), + } + } +} + impl FromIterator for Any { fn from_iter>(iter: I) -> Self { Self::Array(iter.into_iter().collect()) diff --git a/y-octo/src/doc/codec/content.rs b/y-octo/src/doc/codec/content.rs index 9e9ed0c..06f0f98 100644 --- a/y-octo/src/doc/codec/content.rs +++ b/y-octo/src/doc/codec/content.rs @@ -1,7 +1,4 @@ -use std::ops::Deref; - use super::*; -use crate::sync::RwLock; #[derive(Clone)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] @@ -26,6 +23,9 @@ pub(crate) enum Content { }, } +unsafe impl Send for Content {} +unsafe impl Sync for Content {} + impl From for Content { fn from(value: Any) -> Self { match value { @@ -65,10 +65,7 @@ impl PartialEq for Content { ) => key1 == key2 && value1 == value2, (Self::Any(any1), Self::Any(any2)) => any1 == any2, (Self::Doc { guid: guid1, .. }, Self::Doc { guid: guid2, .. }) => guid1 == guid2, - (Self::Type(ty1), Self::Type(ty2)) => match (ty1.get(), ty2.get()) { - (Some(ty1), Some(ty2)) => ty1.read().unwrap().deref() == ty2.read().unwrap().deref(), - _ => false, - }, + (Self::Type(ty1), Self::Type(ty2)) => ty1 == ty2, _ => false, } } @@ -93,10 +90,7 @@ impl std::fmt::Debug for Content { .field("key", key) .field("value", value) .finish(), - Self::Type(arg0) => f - .debug_tuple("Type") - .field(&arg0.get().unwrap().read().unwrap().kind()) - .finish(), + Self::Type(arg0) => f.debug_tuple("Type").field(&arg0.ty().unwrap().kind()).finish(), Self::Any(arg0) => f.debug_tuple("Any").field(arg0).finish(), Self::Doc { guid, opts } => f.debug_struct("Doc").field("guid", guid).field("opts", opts).finish(), } @@ -143,8 +137,7 @@ impl Content { _ => None, }; - let ty = YType::new(kind, tag_name); - Ok(Self::Type(Somr::new(RwLock::new(ty)))) + Ok(Self::Type(YTypeRef::new(kind, tag_name))) } // YType 8 => Ok(Self::Any(Any::read_multiple(decoder)?)), // Any 9 => { @@ -202,12 +195,11 @@ impl Content { .write_var_string(serde_json::to_string(value).map_err(|_| JwstCodecError::DamagedDocumentJson)?)?; } Self::Type(ty) => { - if let Some(ty) = ty.get() { - let ty = ty.read().unwrap(); + if let Some(ty) = ty.ty() { let type_ref = u64::from(ty.kind()); encoder.write_var_u64(type_ref)?; - if matches!(ty.kind, YTypeKind::XMLElement | YTypeKind::XMLHook) { + if matches!(ty.kind(), YTypeKind::XMLElement | YTypeKind::XMLHook) { encoder.write_var_string(ty.name.as_ref().unwrap())?; } } @@ -249,11 +241,11 @@ impl Content { Ok((Self::String(left.to_string()), Self::String(right.to_string()))) } Self::Json(vec) => { - let (left, right) = vec.split_at((diff + 1) as usize); + let (left, right) = vec.split_at(diff as usize); Ok((Self::Json(left.to_owned()), Self::Json(right.to_owned()))) } Self::Any(vec) => { - let (left, right) = vec.split_at((diff + 1) as usize); + let (left, right) = vec.split_at(diff as usize); Ok((Self::Any(left.to_owned()), Self::Any(right.to_owned()))) } Self::Deleted(len) => { @@ -312,19 +304,13 @@ mod tests { key: "key".to_string(), value: Any::Integer(42), }, - Content::Type(Somr::new(RwLock::new(YType::new(YTypeKind::Array, None)))), - Content::Type(Somr::new(RwLock::new(YType::new(YTypeKind::Map, None)))), - Content::Type(Somr::new(RwLock::new(YType::new(YTypeKind::Text, None)))), - Content::Type(Somr::new(RwLock::new(YType::new( - YTypeKind::XMLElement, - Some("test".to_string()), - )))), - Content::Type(Somr::new(RwLock::new(YType::new(YTypeKind::XMLFragment, None)))), - Content::Type(Somr::new(RwLock::new(YType::new( - YTypeKind::XMLHook, - Some("test".to_string()), - )))), - Content::Type(Somr::new(RwLock::new(YType::new(YTypeKind::XMLText, None)))), + Content::Type(YTypeRef::new(YTypeKind::Array, None)), + Content::Type(YTypeRef::new(YTypeKind::Map, None)), + Content::Type(YTypeRef::new(YTypeKind::Text, None)), + Content::Type(YTypeRef::new(YTypeKind::XMLElement, Some("test".to_string()))), + Content::Type(YTypeRef::new(YTypeKind::XMLFragment, None)), + Content::Type(YTypeRef::new(YTypeKind::XMLHook, Some("test".to_string()))), + Content::Type(YTypeRef::new(YTypeKind::XMLText, None)), Content::Any(vec![Any::BigInt64(42), Any::String("Test Any".to_string())]), Content::Doc { guid: "my_guid".to_string(), @@ -357,18 +343,18 @@ mod tests { { let (left, right) = contents[1].split(1).unwrap(); assert!(contents[1].splittable()); - assert_eq!(left, Content::Json(vec![None, Some("test_1".to_string())])); - assert_eq!(right, Content::Json(vec![Some("test_2".to_string())])); + assert_eq!(left, Content::Json(vec![None])); + assert_eq!( + right, + Content::Json(vec![Some("test_1".to_string()), Some("test_2".to_string())]) + ); } { let (left, right) = contents[2].split(1).unwrap(); assert!(contents[2].splittable()); - assert_eq!( - left, - Content::Any(vec![Any::BigInt64(42), Any::String("Test Any".to_string())]) - ); - assert_eq!(right, Content::Any(vec![])); + assert_eq!(left, Content::Any(vec![Any::BigInt64(42)])); + assert_eq!(right, Content::Any(vec![Any::String("Test Any".to_string())])); } { diff --git a/y-octo/src/doc/codec/item.rs b/y-octo/src/doc/codec/item.rs index 26e28ad..ce089aa 100644 --- a/y-octo/src/doc/codec/item.rs +++ b/y-octo/src/doc/codec/item.rs @@ -227,8 +227,6 @@ impl Item { return false; } - // self.content.delete(); - self.flags.set_deleted(); true @@ -238,6 +236,10 @@ impl Item { self.flags.countable() } + pub fn keep(&self) -> bool { + self.flags.keep() + } + pub fn indexable(&self) -> bool { self.countable() && !self.deleted() } @@ -248,6 +250,10 @@ impl Item { Id::new(client, clock + self.len() - 1) } + pub fn right_item(&self) -> ItemRef { + self.right.as_ref().map(|n| n.as_item()).unwrap_or_default() + } + #[allow(dead_code)] #[cfg(any(debug, test))] pub(crate) fn print_left(&self) { @@ -417,8 +423,7 @@ impl Item { encoder.write_item_id(id)?; } Parent::Type(ty) => { - if let Some(ty) = ty.get() { - let ty = ty.read().unwrap(); + if let Some(ty) = ty.ty() { if let Some(item) = ty.item.get() { encoder.write_var_u64(0)?; encoder.write_item_id(&item.id)?; diff --git a/y-octo/src/doc/codec/refs.rs b/y-octo/src/doc/codec/refs.rs index 99304a5..ac83196 100644 --- a/y-octo/src/doc/codec/refs.rs +++ b/y-octo/src/doc/codec/refs.rs @@ -80,7 +80,9 @@ impl Node { let item = Somr::new(Item::read(decoder, id, info, first_5_bit)?); if let Content::Type(ty) = item.get().unwrap().content.as_ref() { - ty.get().unwrap().write().unwrap().item = item.clone(); + if let Some(mut ty) = ty.ty_mut() { + ty.item = item.clone(); + } } Ok(Node::Item(item)) diff --git a/y-octo/src/doc/codec/update.rs b/y-octo/src/doc/codec/update.rs index 2848d17..b7af5eb 100644 --- a/y-octo/src/doc/codec/update.rs +++ b/y-octo/src/doc/codec/update.rs @@ -6,6 +6,9 @@ use std::{ use super::*; use crate::doc::StateVector; +const CLIENTS_SAFE_CAPACITY: u64 = 1024; +const STRUCTS_SAFE_CAPACITY: u64 = 10240; + #[derive(Debug, Default, Clone)] pub struct Update { pub(crate) structs: HashMap>, @@ -25,13 +28,21 @@ impl CrdtRead for Update { fn read(decoder: &mut R) -> JwstCodecResult { let num_of_clients = decoder.read_var_u64()?; - let mut map = HashMap::with_capacity(num_of_clients as usize); + let mut map = HashMap::with_capacity(if num_of_clients > CLIENTS_SAFE_CAPACITY { + CLIENTS_SAFE_CAPACITY + } else { + num_of_clients + } as usize); for _ in 0..num_of_clients { let num_of_structs = decoder.read_var_u64()?; let client = decoder.read_var_u64()?; let mut clock = decoder.read_var_u64()?; - let mut structs = VecDeque::with_capacity(num_of_structs as usize); + let mut structs = VecDeque::with_capacity(if num_of_structs > STRUCTS_SAFE_CAPACITY { + STRUCTS_SAFE_CAPACITY + } else { + num_of_structs + } as usize); for _ in 0..num_of_structs { let struct_info = Node::read(decoder, Id::new(client, clock))?; diff --git a/y-octo/src/doc/common/range.rs b/y-octo/src/doc/common/range.rs index 1f9e1ea..15c75ef 100644 --- a/y-octo/src/doc/common/range.rs +++ b/y-octo/src/doc/common/range.rs @@ -187,6 +187,46 @@ impl OrderRange { } } +impl<'a> IntoIterator for &'a OrderRange { + type Item = Range; + type IntoIter = OrderRangeIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + OrderRangeIter { range: self, idx: 0 } + } +} + +pub struct OrderRangeIter<'a> { + range: &'a OrderRange, + idx: usize, +} + +impl<'a> Iterator for OrderRangeIter<'a> { + type Item = Range; + + fn next(&mut self) -> Option { + match self.range { + OrderRange::Range(range) => { + if self.idx == 0 { + self.idx += 1; + Some(range.clone()) + } else { + None + } + } + OrderRange::Fragment(ranges) => { + if self.idx < ranges.len() { + let range = ranges[self.idx].clone(); + self.idx += 1; + Some(range) + } else { + None + } + } + } + } +} + #[cfg(test)] mod tests { use super::OrderRange; @@ -265,4 +305,15 @@ mod tests { range.merge(vec![(10..20), (30..40)].into()); assert_eq!(range, OrderRange::Range(0..40)); } + + #[test] + fn iter() { + let range: OrderRange = vec![(0..10), (20..30)].into(); + + assert_eq!(range.into_iter().collect::>(), vec![(0..10), (20..30)]); + + let range: OrderRange = OrderRange::Range(0..10); + + assert_eq!(range.into_iter().collect::>(), vec![(0..10)]); + } } diff --git a/y-octo/src/doc/common/somr.rs b/y-octo/src/doc/common/somr.rs index a1c2889..2091d82 100644 --- a/y-octo/src/doc/common/somr.rs +++ b/y-octo/src/doc/common/somr.rs @@ -1,8 +1,10 @@ use std::{ + cell::UnsafeCell, fmt::{self, Write}, hash::{Hash, Hasher}, marker::PhantomData, mem, + ops::{Deref, DerefMut}, ptr::NonNull, }; @@ -25,13 +27,32 @@ pub(crate) struct Owned(NonNull>); pub(crate) struct Ref(NonNull>); pub(crate) struct SomrInner { - data: Option, + data: Option>, /// increase the size when we really meet the the secenerio with refs more /// then u16::MAX(65535) times refs: AtomicU32, _marker: PhantomData>, } +pub(crate) struct InnerRefMut<'a, T> { + inner: NonNull, + _marker: PhantomData<&'a mut T>, +} + +impl<'a, T> Deref for InnerRefMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.inner.as_ptr() } + } +} + +impl<'a, T> DerefMut for InnerRefMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.inner.as_ptr() } + } +} + unsafe impl Send for Somr {} unsafe impl Sync for Somr {} @@ -44,7 +65,7 @@ impl Default for Somr { impl Somr { pub fn new(data: T) -> Self { let inner = Box::new(SomrInner { - data: Some(data), + data: Some(UnsafeCell::new(data)), refs: AtomicU32::new(1), _marker: PhantomData, }); @@ -57,15 +78,28 @@ impl Somr { } } +impl SomrInner { + fn data_ref(&self) -> Option<&T> { + self.data.as_ref().map(|x| unsafe { &*x.get() }) + } + + fn data_mut(&self) -> Option> { + self.data.as_ref().map(|x| InnerRefMut { + inner: unsafe { NonNull::new_unchecked(x.get()) }, + _marker: PhantomData, + }) + } +} + impl Somr { #[inline] pub fn is_none(&self) -> bool { - self.dangling() || self.inner().data.is_none() + self.dangling() || self.inner().data_ref().is_none() } #[inline] pub fn is_some(&self) -> bool { - !self.dangling() && self.inner().data.is_some() + !self.dangling() && self.inner().data_ref().is_some() } pub fn get(&self) -> Option<&T> { @@ -73,7 +107,7 @@ impl Somr { return None; } - self.inner().data.as_ref() + self.inner().data_ref() } pub unsafe fn get_unchecked(&self) -> &T { @@ -81,7 +115,7 @@ impl Somr { panic!("Try to visit Somr data that has already been dropped.") } - match &self.inner().data { + match &self.inner().data_ref() { Some(data) => data, None => { panic!("Try to unwrap on None") @@ -89,23 +123,32 @@ impl Somr { } } - #[allow(dead_code)] - pub fn get_mut(&self) -> Option<&mut T> { + #[allow(unused)] + pub fn get_mut(&mut self) -> Option<&mut T> { if !self.is_owned() || self.dangling() { return None; } let inner = self.inner_mut(); - inner.data.as_mut() + inner.data.as_mut().map(|x| x.get_mut()) } - #[allow(clippy::mut_from_ref)] - pub unsafe fn get_mut_unchecked(&self) -> &mut T { + #[allow(unused)] + pub unsafe fn get_mut_from_ref(&self) -> Option> { + if !self.is_owned() || self.dangling() { + return None; + } + + let inner = self.inner_mut(); + inner.data_mut() + } + + pub unsafe fn get_mut_unchecked(&self) -> InnerRefMut<'_, T> { if self.dangling() { panic!("Try to visit Somr data that has already been dropped.") } - match &mut self.inner_mut().data { + match self.inner_mut().data_mut() { Some(data) => data, None => { panic!("Try to unwrap on None") @@ -142,13 +185,18 @@ impl Somr { } #[inline] - pub(crate) fn ptr(&self) -> NonNull> { + pub fn ptr(&self) -> NonNull> { match self { Somr::Owned(ptr) => ptr.0, Somr::Ref(ptr) => ptr.0, } } + #[inline] + pub fn ptr_eq(&self, other: &Self) -> bool { + self.ptr().as_ptr() as usize == other.ptr().as_ptr() as usize + } + #[inline] fn dangling(&self) -> bool { is_dangling(self.ptr()) @@ -238,7 +286,7 @@ impl PartialEq for Somr { impl PartialEq for SomrInner { fn eq(&self, other: &Self) -> bool { - self.data == other.data + self.data_ref() == other.data_ref() } } @@ -366,7 +414,7 @@ mod tests { #[test] fn acquire_mut_ref() { loom_model!({ - let five = Somr::new(5); + let mut five = Somr::new(5); *five.get_mut().unwrap() += 1; assert_eq!(five.get(), Some(&6)); @@ -375,7 +423,7 @@ mod tests { // only owner can mut ref assert!(five_ref.get().is_some()); - assert!(five_ref.get_mut().is_none()); + assert!(unsafe { five_ref.get_mut_from_ref() }.is_none()); drop(five); }); @@ -442,4 +490,20 @@ mod tests { assert!(!five.is_owned()); }); } + + // This is UB if we didn't use `UnsafeCell` in `Somr` + #[test] + fn test_inner_mut() { + loom_model!({ + let five = Somr::new(5); + fn add(a: &Somr, b: &Somr) { + unsafe { a.get_mut_from_ref() } + .map(|mut x| *x += *b.get().unwrap()) + .unwrap(); + } + + add(&five, &five); + assert_eq!(five.get().copied().unwrap(), 10); + }); + } } diff --git a/y-octo/src/doc/document.rs b/y-octo/src/doc/document.rs index 57078d3..5777171 100644 --- a/y-octo/src/doc/document.rs +++ b/y-octo/src/doc/document.rs @@ -1,21 +1,111 @@ +use std::collections::HashMap; + use super::{publisher::DocPublisher, store::StoreRef, *}; use crate::sync::{Arc, RwLock}; -#[derive(Clone, Default)] +/// [DocOptions] used to create a new [Doc] +/// +/// ``` +/// let doc = DocOptions::new() +/// .with_client_id(1) +/// .with_guid("guid".into()) +/// .auto_gc(true) +/// .build(); +/// +/// assert!(doc.guid(), "guid") +/// ``` +#[derive(Clone, Debug)] pub struct DocOptions { - pub guid: Option, - pub client: Option, + pub guid: String, + pub client_id: u64, + pub gc: bool, +} + +impl Default for DocOptions { + fn default() -> Self { + if cfg!(test) { + Self { + client_id: 1, + guid: "test".into(), + gc: true, + } + } else { + Self { + client_id: rand::random(), + guid: nanoid::nanoid!(), + gc: true, + } + } + } +} + +impl DocOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn with_client_id(mut self, client_id: u64) -> Self { + self.client_id = client_id; + self + } + + pub fn with_guid(mut self, guid: String) -> Self { + self.guid = guid; + self + } + + pub fn auto_gc(mut self, gc: bool) -> Self { + self.gc = gc; + self + } + + pub fn build(self) -> Doc { + Doc::with_options(self) + } +} + +impl From for Any { + fn from(value: DocOptions) -> Self { + Any::Object(HashMap::from([ + ("gc".into(), value.gc.into()), + ("guid".into(), value.guid.into()), + ])) + } +} + +impl TryFrom for DocOptions { + type Error = JwstCodecError; + + fn try_from(value: Any) -> Result { + match value { + Any::Object(map) => { + let mut options = DocOptions::default(); + for (key, value) in map { + match key.as_str() { + "gc" => { + options.gc = bool::try_from(value)?; + } + "guid" => { + options.guid = String::try_from(value)?; + } + _ => {} + } + } + + Ok(options) + } + _ => Err(JwstCodecError::UnexpectedType("Object")), + } + } } #[derive(Debug, Clone)] pub struct Doc { client_id: u64, - // random id for each doc, use in sub doc - // TODO: use function in code - #[allow(dead_code)] - guid: String, - pub(super) store: StoreRef, - pub publisher: Arc, + opts: DocOptions, + + pub(crate) store: StoreRef, + pub(crate) publisher: Arc, } unsafe impl Send for Doc {} @@ -23,16 +113,7 @@ unsafe impl Sync for Doc {} impl Default for Doc { fn default() -> Self { - let client_id = rand::random(); - let store = Arc::new(RwLock::new(DocStore::with_client(client_id))); - let publisher = Arc::new(DocPublisher::new(store.clone())); - - Self { - client_id, - guid: nanoid!(), - store, - publisher, - } + Doc::new() } } @@ -43,36 +124,36 @@ impl PartialEq for Doc { } impl Doc { + pub fn new() -> Self { + Self::with_options(DocOptions::default()) + } + pub fn with_options(options: DocOptions) -> Self { - let client = options.client.unwrap_or_else(rand::random); - let store = Arc::new(RwLock::new(DocStore::with_client(client))); + let store = Arc::new(RwLock::new(DocStore::with_client(options.client_id))); let publisher = Arc::new(DocPublisher::new(store.clone())); Self { - client_id: client, + client_id: options.client_id, + opts: options, store, - guid: options.guid.unwrap_or_else(|| nanoid!()), publisher, } } pub fn with_client(client_id: u64) -> Self { - let store = Arc::new(RwLock::new(DocStore::with_client(client_id))); - let publisher = Arc::new(DocPublisher::new(store.clone())); - Self { - client_id, - store, - guid: nanoid!(), - publisher, - } + DocOptions::new().with_client_id(client_id).build() } pub fn client(&self) -> Client { self.client_id } + pub fn options(&self) -> &DocOptions { + &self.opts + } + pub fn guid(&self) -> &str { - self.guid.as_str() + self.opts.guid.as_str() } pub fn new_from_binary(binary: Vec) -> JwstCodecResult { @@ -101,8 +182,8 @@ impl Doc { for (mut s, offset) in update.iter(store.get_state_vector()) { if let Node::Item(item) = &mut s { debug_assert!(item.is_owned()); - let item = unsafe { item.get_mut_unchecked() }; - store.repair(item, self.store.clone())?; + let mut item = unsafe { item.get_mut_unchecked() }; + store.repair(&mut item, self.store.clone())?; } store.integrate(s, offset, None)?; } @@ -142,6 +223,7 @@ impl Doc { } else { // need to turn all pending state into update for later iteration update.drain_pending_state(); + retry = false; }; } @@ -227,6 +309,10 @@ impl Doc { pub fn unsubscribe_all(&self) { self.publisher.unsubscribe_all(); } + + pub fn gc(&self) -> JwstCodecResult<()> { + self.store.write().unwrap().optimize() + } } #[cfg(test)] @@ -247,13 +333,8 @@ mod tests { let binary_from_yrs = trx.encode_update_v1().unwrap(); - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ - let doc = Doc::new_from_binary_with_options(binary_from_yrs.clone(), options.clone()).unwrap(); + let doc = Doc::new_from_binary(binary_from_yrs.clone()).unwrap(); let binary = doc.encode_update_v1().unwrap(); assert_eq!(binary_from_yrs, binary); @@ -262,36 +343,18 @@ mod tests { #[test] fn test_encode_state_as_update() { - let options_left = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - let options_right = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - - let yrs_options_left = Options { - client_id: rand::random(), - guid: nanoid::nanoid!().into(), - ..Default::default() - }; - - let yrs_options_right = Options { - client_id: rand::random(), - guid: nanoid::nanoid!().into(), - ..Default::default() - }; + let yrs_options_left = Options::default(); + let yrs_options_right = Options::default(); loom_model!({ let (binary, binary_new) = if cfg!(miri) { - let doc = Doc::with_options(options_left.clone()); + let doc = Doc::new(); let mut map = doc.get_or_create_map("abc").unwrap(); map.insert("a", 1).unwrap(); let binary = doc.encode_update_v1().unwrap(); - let doc_new = Doc::with_options(options_right.clone()); + let doc_new = Doc::new(); let mut array = doc_new.get_or_create_array("array").unwrap(); array.insert(0, "array_value").unwrap(); let binary_new = doc.encode_update_v1().unwrap(); @@ -314,8 +377,8 @@ mod tests { (binary, binary_new) }; - let mut doc = Doc::new_from_binary_with_options(binary.clone(), options_left.clone()).unwrap(); - let mut doc_new = Doc::new_from_binary_with_options(binary_new.clone(), options_right.clone()).unwrap(); + let mut doc = Doc::new_from_binary(binary.clone()).unwrap(); + let mut doc_new = Doc::new_from_binary(binary_new.clone()).unwrap(); let diff_update = doc_new.encode_state_as_update_v1(&doc.get_state_vector()).unwrap(); @@ -331,13 +394,7 @@ mod tests { #[test] #[cfg_attr(any(miri, loom), ignore)] fn test_array_create() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - - let yrs_options = - yrs::Options::with_guid_and_client_id(options.guid.clone().unwrap().into(), options.client.unwrap()); + let yrs_options = yrs::Options::default(); let json = serde_json::json!([42.0, -42.0, true, false, "hello", "world", [1.0]]); @@ -362,7 +419,7 @@ mod tests { }; let binary = { - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut array = doc.get_or_create_array("abc").unwrap(); array.insert(0, 42).unwrap(); array.insert(1, -42).unwrap(); @@ -386,7 +443,7 @@ mod tests { assert_json_diff::assert_json_eq!(array.to_json(&trx), json); - let mut doc = Doc::with_options(options); + let mut doc = Doc::new(); let array = doc.get_or_create_array("abc").unwrap(); doc.apply_update_from_binary(binary).unwrap(); diff --git a/y-octo/src/doc/store.rs b/y-octo/src/doc/store.rs index aa135ba..0e2e9e6 100644 --- a/y-octo/src/doc/store.rs +++ b/y-octo/src/doc/store.rs @@ -1,7 +1,7 @@ use std::{ collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, mem, - ops::Range, + ops::{Deref, Range}, }; use super::*; @@ -25,6 +25,7 @@ pub(crate) struct DocStore { // we store it here to keep the ownership inside store without being released. pub dangling_types: HashMap, pub pending: Option, + pub last_optimized_state: StateVector, } pub(crate) type StoreRef = Arc>; @@ -77,7 +78,7 @@ impl DocStore { state } - pub fn add_item(&mut self, item: Node) -> JwstCodecResult { + pub fn add_node(&mut self, item: Node) -> JwstCodecResult { let client_id = item.client(); match self.items.entry(client_id) { Entry::Occupied(mut entry) => { @@ -102,7 +103,7 @@ impl DocStore { } /// binary search struct info on a sorted array - pub fn get_item_index(items: &VecDeque, clock: Clock) -> Option { + pub fn get_node_index(items: &VecDeque, clock: Clock) -> Option { let mut left = 0; let mut right = items.len() - 1; let middle = &items[right]; @@ -139,7 +140,9 @@ impl DocStore { let item = Somr::new(Item::new(id, content, left, right, parent, parent_sub)); if let Content::Type(ty) = item.get().unwrap().content.as_ref() { - ty.get().unwrap().write().unwrap().item = item.clone(); + if let Some(mut ty) = ty.ty_mut() { + ty.item = item.clone(); + } } item @@ -152,7 +155,7 @@ impl DocStore { pub fn get_node_with_idx>(&self, id: I) -> Option<(Node, usize)> { let id = id.into(); if let Some(items) = self.items.get(&id.client) { - if let Some(index) = Self::get_item_index(items, id.clock) { + if let Some(index) = Self::get_node_index(items, id.clock) { return items.get(index).map(|item| (item.clone(), index)); } } @@ -166,7 +169,7 @@ impl DocStore { let id = id.into(); if let Some(items) = self.items.get_mut(&id.client) { - if let Some(idx) = Self::get_item_index(items, id.clock) { + if let Some(idx) = Self::get_node_index(items, id.clock) { return Self::split_node_at(items, idx, diff); } } @@ -186,15 +189,15 @@ impl DocStore { // we make sure store is the only entry of mutating an item, // and we already hold mutable reference of store, so it's safe to do so unsafe { - let left_item = raw_left_item.get_mut_unchecked(); - let right_item = right_item.get_mut_unchecked(); + let mut left_item = raw_left_item.get_mut_unchecked(); + let mut right_item = right_item.get_mut_unchecked(); left_item.content = new_left_item.get_unchecked().content.clone(); // we had the correct left/right content // now build the references let right_right_ref = ItemRef::from(&left_item.right); right_item.left = if right_right_ref.is_some() { - let right_right = right_right_ref.get_mut_unchecked(); + let mut right_right = right_right_ref.get_mut_unchecked(); right_right.left.replace(right_node.clone()) } else { Some(node.clone()) @@ -218,7 +221,7 @@ impl DocStore { pub fn split_at_and_get_right>(&mut self, id: I) -> JwstCodecResult { let id = id.into(); if let Some(items) = self.items.get_mut(&id.client) { - if let Some(index) = Self::get_item_index(items, id.clock) { + if let Some(index) = Self::get_node_index(items, id.clock) { let item = items.get(index).unwrap().clone(); let offset = id.clock - item.clock(); if offset > 0 && item.is_item() { @@ -236,7 +239,7 @@ impl DocStore { pub fn split_at_and_get_left>(&mut self, id: I) -> JwstCodecResult { let id = id.into(); if let Some(items) = self.items.get_mut(&id.client) { - if let Some(index) = Self::get_item_index(items, id.clock) { + if let Some(index) = Self::get_node_index(items, id.clock) { let item = items.get(index).unwrap().clone(); let offset = id.clock - item.clock(); if offset != item.len() - 1 && !item.is_gc() { @@ -271,16 +274,17 @@ impl DocStore { } // only for creating named type - pub fn get_or_create_type(&mut self, name: &str) -> YTypeRef { + pub fn get_or_create_type(&mut self, store_ref: &StoreRef, name: &str) -> YTypeRef { match self.types.entry(name.to_string()) { Entry::Occupied(e) => e.get().clone(), Entry::Vacant(e) => { - let ty = Somr::new(RwLock::new(YType { - kind: YTypeKind::Unknown, - root_name: Some(name.to_string()), - ..Default::default() - })); + let mut inner = YType::new(YTypeKind::Unknown, None); + inner.root_name = Some(name.to_string()); + let ty = YTypeRef { + store: Arc::downgrade(store_ref), + inner: Somr::new(RwLock::new(inner)), + }; let ty_ref = ty.clone(); e.insert(ty); ty_ref @@ -310,10 +314,7 @@ impl DocStore { // doc.get_or_create_text("content"); // ^^^^^^^ Parent::String("content") Some(Parent::String(str)) => { - let ty = self.get_or_create_type(str); - if let Some(ty) = ty.get() { - ty.write().unwrap().store = Arc::downgrade(&store_ref); - } + let ty = self.get_or_create_type(&store_ref, str); item.parent.replace(Parent::Type(ty)); } // type as item @@ -328,9 +329,6 @@ impl DocStore { Some(Node::Item(parent_item)) => { match &parent_item.get().unwrap().content.as_ref() { Content::Type(ty) => { - if let Some(ty) = ty.get() { - ty.write().unwrap().store = Arc::downgrade(&store_ref); - } item.parent.replace(Parent::Type(ty.clone())); } _ => { @@ -361,12 +359,19 @@ impl DocStore { // assign store in ytype to ensure store exists if a ytype not has any children if let Content::Type(ty) = Arc::make_mut(&mut item.content) { - if let Some(ty) = ty.get() { - ty.write().unwrap().store = Arc::downgrade(&store_ref); - } - - if ty.is_owned() { - self.dangling_types.insert(ty.ptr().as_ptr() as usize, ty.swap_take()); + ty.store = Arc::downgrade(&store_ref); + + // we keep ty owner in dangling_types so the delete of any type will not make it + // dropped + if ty.inner.is_owned() { + let owned_inner = ty.inner.swap_take(); + self.dangling_types.insert( + ty.inner.ptr().as_ptr() as usize, + YTypeRef { + store: ty.store.clone(), + inner: owned_inner, + }, + ); } else { return Err(JwstCodecError::InvalidParent); } @@ -375,7 +380,7 @@ impl DocStore { Ok(()) } - pub(crate) fn integrate(&mut self, mut node: Node, offset: u64, parent: Option<&mut YType>) -> JwstCodecResult { + pub fn integrate(&mut self, mut node: Node, offset: u64, parent: Option<&mut YType>) -> JwstCodecResult { match &mut node { Node::Item(item) => { assert!( @@ -387,7 +392,7 @@ impl DocStore { // before we integrate struct into store, // the struct => Arc is owned reference actually, // no one else refer to such item yet, we can safely mutable refer to it now. - let this = unsafe { item.get_mut_unchecked() }; + let this = &mut *unsafe { item.get_mut_unchecked() }; if offset > 0 { this.id.clock += offset; @@ -401,9 +406,8 @@ impl DocStore { let mut parent_lock: Option> = None; let parent = if let Some(p) = parent { p - } else if let Some(ty) = ty.get() { - let lock = ty.write().unwrap(); - parent_lock = Some(lock); + } else if let Some(ty) = ty.ty_mut() { + parent_lock = Some(ty); parent_lock.as_deref_mut().unwrap() } else { return Ok(()); @@ -432,7 +436,6 @@ impl DocStore { .map .as_ref() .and_then(|m| m.get(parent_sub).cloned()) - .map(|s| s.as_item()) .unwrap_or(Somr::none()) } else { parent.start.clone() @@ -485,7 +488,7 @@ impl DocStore { if left.is_some() { unsafe { // SAFETY: we get store write lock, no way the left get dropped by owner - let left = left.get_mut_unchecked(); + let mut left = left.get_mut_unchecked(); right = left.right.replace(Node::Item(item.clone())).into(); } this.left = Some(Node::Item(left)); @@ -496,7 +499,7 @@ impl DocStore { .map .as_ref() .and_then(|map| map.get(parent_sub)) - .map(|n| n.head()) + .map(|n| Node::Item(n.clone()).head()) .into() } else { mem::replace(&mut parent.start, item.clone()) @@ -508,7 +511,7 @@ impl DocStore { if right.is_some() { unsafe { // SAFETY: we get store write lock, no way the left get dropped by owner - let right = right.get_mut_unchecked(); + let mut right = right.get_mut_unchecked(); right.left = Some(Node::Item(item.clone())); } this.right = Some(Node::Item(right)) @@ -518,10 +521,10 @@ impl DocStore { parent .map .get_or_insert(HashMap::default()) - .insert(parent_sub.to_string(), Node::Item(item.clone())); + .insert(parent_sub.to_string(), item.clone()); if let Some(left) = &this.left { - self.delete(left, Some(parent)); + self.delete_node(left, Some(parent)); } } } @@ -530,7 +533,7 @@ impl DocStore { // should delete if parent_deleted || this.parent_sub.is_some() && this.right.is_some() { - self.delete(&Node::Item(item.clone()), Some(parent)); + self.delete_node(&Node::Item(item.clone()), Some(parent)); } else { // adjust parent length if this.parent_sub.is_none() { @@ -551,25 +554,52 @@ impl DocStore { // skip ignored } } - self.add_item(node) + self.add_node(node) } - pub(crate) fn delete_item(item: &Item, parent: Option<&mut YType>) { + pub fn delete_item(&mut self, item: &Item, parent: Option<&mut YType>) { + Self::delete_item_inner(&mut self.delete_set, item, parent); + } + + fn delete_item_inner(delete_set: &mut DeleteSet, item: &Item, parent: Option<&mut YType>) { if !item.delete() { return; } + delete_set.add(item.id.client, item.id.clock, item.len()); + if item.parent_sub.is_none() && item.countable() { if let Some(parent) = parent { parent.len -= item.len(); } else if let Some(Parent::Type(ty)) = &item.parent { - ty.get().unwrap().write().unwrap().len -= item.len(); + ty.ty_mut().unwrap().len -= item.len(); } } match item.content.as_ref() { - Content::Type(_) => { - // TODO: remove all type items + Content::Type(ty) => { + if let Some(mut ty) = ty.ty_mut() { + // items in ty are all refs, not owned + let mut item_ref = ty.start.clone(); + while let Some(item) = item_ref.get() { + // delete_set + Self::delete_item_inner(delete_set, item, Some(&mut ty)); + + if let Some(right) = &item.right { + item_ref = right.as_item(); + } else { + item_ref = Somr::none(); + } + } + + if let Some(map) = ty.map.take() { + for (_, item) in map { + if let Some(item) = item.get() { + Self::delete_item_inner(delete_set, item, Some(&mut ty)); + } + } + } + } } Content::Doc { .. } => { // TODO: remove subdoc @@ -578,10 +608,9 @@ impl DocStore { } } - pub(crate) fn delete(&mut self, struct_info: &Node, parent: Option<&mut YType>) { + pub fn delete_node(&mut self, struct_info: &Node, parent: Option<&mut YType>) { if let Some(item) = struct_info.as_item().get() { - self.delete_set.add(item.id.client, item.id.clock, item.len()); - Self::delete_item(item, parent); + self.delete_item(item, parent); } } @@ -590,38 +619,37 @@ impl DocStore { let end = range.end; if let Some(items) = self.items.get_mut(&client) { - if let Some(mut idx) = DocStore::get_item_index(items, start) { + if let Some(mut idx) = DocStore::get_node_index(items, start) { { // id.clock <= range.start < id.end // need to split the item and delete the right part // -----item----- // ^start - let struct_info = &items[idx]; - let id = struct_info.id(); + let node = &items[idx]; + let id = node.id(); - if !struct_info.deleted() && id.clock < start { + if !node.deleted() && id.clock < start { DocStore::split_node_at(items, idx, start - id.clock)?; idx += 1; } }; while idx < items.len() { - let struct_info = items[idx].clone(); - let id = struct_info.id(); - - if let Some(item) = struct_info.as_item().get() { - if !item.deleted() && id.clock < end { - // need to split the item - // -----item----- - // ^end - if end < id.clock + struct_info.len() { - DocStore::split_node_at(items, idx, end - id.clock)?; - } + let node = items[idx].clone(); + let id = node.id(); + + if id.clock < end { + if !node.deleted() { + if let Some(item) = node.as_item().get() { + // need to split the item + // -----item----- + // ^end + if end < id.clock + node.len() { + DocStore::split_node_at(items, idx, end - id.clock)?; + } - self.delete_set.add(client, id.clock, item.len()); - Self::delete_item(item, None); - } else { - break; + Self::delete_item_inner(&mut self.delete_set, item, None); + } } } else { break; @@ -691,7 +719,7 @@ impl DocStore { // the smallest clock in items may exceed the clock let clock = items.front().unwrap().id().clock.max(clock); - if let Some(index) = Self::get_item_index(items, clock) { + if let Some(index) = Self::get_node_index(items, clock) { let first_block = items.get(index).unwrap(); let offset = first_block.clock() - clock; if offset != 0 { @@ -724,6 +752,194 @@ impl DocStore { delete_set } + + /// Optimize the memory usage of store + pub fn optimize(&mut self) -> JwstCodecResult { + // 1. gc delete set + self.gc_delete_set()?; + // 2. merge delete set (in our delete set impl, which is based on `OrderRange` + // has already have auto-merge functionality) pass + // 3. merge same content siblings, e.g contentString + ContentString + self.make_continuous() + } + + fn gc_delete_set(&mut self) -> JwstCodecResult<()> { + for (client, deletes) in self.delete_set.deref() { + for range in deletes { + let start = range.start; + let end = range.end; + let items = self.items.get_mut(client).unwrap(); + if let Some(mut idx) = Self::get_node_index(items, start) { + while idx < items.len() { + if let Node::Item(item) = items[idx].clone() { + let item = unsafe { item.get_unchecked() }; + + if item.id.clock >= end { + break; + } + + if !item.keep() { + Self::gc_item(items, idx, false)?; + } + } + + idx += 1; + } + } + } + } + + Ok(()) + } + + fn gc_item_by_id(items: &mut VecDeque, id: Id, replace: bool) -> JwstCodecResult { + if let Some(idx) = Self::get_node_index(items, id.clock) { + Self::gc_item(items, idx, replace)?; + } + + Ok(()) + } + + fn gc_item(items: &mut VecDeque, idx: usize, replace: bool) -> JwstCodecResult { + if let Node::Item(item_ref) = items[idx].clone() { + let item = unsafe { item_ref.get_unchecked() }; + + if !item.deleted() { + return Err(JwstCodecError::Unexpected); + } + + Self::gc_content(items, &item.content)?; + + if replace { + let _ = mem::replace(&mut items[idx], Node::new_gc(item.id, item.len())); + } else { + let mut item = unsafe { item_ref.get_mut_unchecked() }; + item.content = Arc::new(Content::Deleted(item.len())); + } + } + + Ok(()) + } + + fn gc_content(items: &mut VecDeque, content: &Content) -> JwstCodecResult { + if let Content::Type(ty) = content { + if let Some(mut ty) = ty.ty_mut() { + // items in ty are all refs, not owned + let mut item_ref = ty.start.clone(); + while let Some(item) = item_ref.get() { + let id = item.id; + + // we need to iter to right first, because we may delete the owned item + // by replacing it with [Node::GC] + if let Some(right) = &item.right { + item_ref = right.as_item(); + } else { + item_ref = Somr::none(); + } + + Self::gc_item_by_id(items, id, true)?; + } + ty.start = Somr::none(); + + if let Some(map) = ty.map.take() { + for (_, item) in map { + if let Some(item) = item.get() { + Self::gc_item_by_id(items, item.id, true)?; + } + } + } + } + } + + Ok(()) + } + + fn make_continuous(&mut self) -> JwstCodecResult { + let state = self.get_state_vector(); + + for (client, state) in state.iter() { + let before_state = self.last_optimized_state.get(client); + if before_state == *state { + continue; + } + + let nodes = self.items.get_mut(client).unwrap(); + let first_change = Self::get_node_index(nodes, before_state).unwrap_or(1).max(1); + let mut idx = nodes.len() - 1; + + while idx > 0 && idx >= first_change { + idx = idx.saturating_sub(Self::merge_with_lefts(nodes, idx)? + 1); + } + } + + self.last_optimized_state = state; + Ok(()) + } + + fn merge_with_lefts(nodes: &mut VecDeque, idx: usize) -> JwstCodecResult { + let mut pos = idx; + loop { + if pos == 0 { + break; + } + + let right = nodes.get(pos).unwrap().clone(); + let left = nodes.get_mut(pos - 1).unwrap(); + + match (left, right) { + (Node::GC(left), Node::GC(right)) => { + left.len += right.len; + } + (Node::Skip(left), Node::Skip(right)) => { + left.len += right.len; + } + (Node::Item(lref), Node::Item(rref)) => { + let mut litem = unsafe { lref.get_mut_unchecked() }; + let mut ritem = unsafe { rref.get_mut_unchecked() }; + + if litem.id.client != ritem.id.client + // not same delete status + || litem.deleted() != ritem.deleted() + // not clock continuous + || litem.id.clock + litem.len() != ritem.id.clock + // not insertion continuous + || Some(litem.last_id()) != ritem.origin_left_id + // not insertion continuous + || litem.origin_right_id != ritem.origin_right_id + // not runtime continuous + || litem.right_item() != rref + { + break; + } + + match (Arc::make_mut(&mut litem.content), Arc::make_mut(&mut ritem.content)) { + (Content::Deleted(l), Content::Deleted(r)) => { + *l += *r; + } + (Content::Json(l), Content::Json(r)) => { + l.extend(r.drain(0..)); + } + (Content::String(l), Content::String(r)) => { + *l += r; + } + (Content::Any(l), Content::Any(r)) => { + l.extend(r.drain(0..)); + } + _ => { + break; + } + } + } + _ => { + break; + } + } + + pos -= 1; + } + nodes.drain(pos + 1..=idx); + Ok(idx - pos) + } } #[cfg(test)] @@ -800,13 +1016,13 @@ mod tests { let struct_info3_err = Node::new_skip(Id::new(1, 5), 1); let struct_info3 = Node::new_skip(Id::new(1, 6), 1); - assert!(doc_store.add_item(struct_info1.clone()).is_ok()); - assert!(doc_store.add_item(struct_info2).is_ok()); + assert!(doc_store.add_node(struct_info1.clone()).is_ok()); + assert!(doc_store.add_node(struct_info2).is_ok()); assert_eq!( - doc_store.add_item(struct_info3_err), + doc_store.add_node(struct_info3_err), Err(JwstCodecError::StructClockInvalid { expect: 6, actually: 5 }) ); - assert!(doc_store.add_item(struct_info3.clone()).is_ok()); + assert!(doc_store.add_node(struct_info3.clone()).is_ok()); assert_eq!( doc_store.get_state(struct_info1.client()), struct_info3.clock() + struct_info3.len() @@ -819,7 +1035,7 @@ mod tests { loom_model!({ let mut doc_store = DocStore::with_client(1); let struct_info = Node::new_gc(Id::new(1, 0), 10); - doc_store.add_item(struct_info.clone()).unwrap(); + doc_store.add_node(struct_info.clone()).unwrap(); assert_eq!(doc_store.get_node(Id::new(1, 9)), Some(struct_info)); }); @@ -828,8 +1044,8 @@ mod tests { let mut doc_store = DocStore::with_client(1); let struct_info1 = Node::new_gc(Id::new(1, 0), 10); let struct_info2 = Node::new_gc(Id::new(1, 10), 20); - doc_store.add_item(struct_info1).unwrap(); - doc_store.add_item(struct_info2.clone()).unwrap(); + doc_store.add_node(struct_info1).unwrap(); + doc_store.add_node(struct_info2.clone()).unwrap(); assert_eq!(doc_store.get_node(Id::new(1, 25)), Some(struct_info2)); }); @@ -844,8 +1060,8 @@ mod tests { let mut doc_store = DocStore::with_client(1); let struct_info1 = Node::new_gc(Id::new(1, 0), 10); let struct_info2 = Node::new_gc(Id::new(1, 10), 20); - doc_store.add_item(struct_info1).unwrap(); - doc_store.add_item(struct_info2).unwrap(); + doc_store.add_node(struct_info1).unwrap(); + doc_store.add_node(struct_info2).unwrap(); assert_eq!(doc_store.get_node(Id::new(1, 35)), None); }); @@ -895,8 +1111,8 @@ mod tests { .build(), )); let s1_ref = struct_info1.clone(); - doc_store.add_item(struct_info1).unwrap(); - doc_store.add_item(struct_info2).unwrap(); + doc_store.add_node(struct_info1).unwrap(); + doc_store.add_node(struct_info2).unwrap(); let s1 = doc_store.get_node(Id::new(1, 0)).unwrap(); assert_eq!(s1, s1_ref); @@ -910,4 +1126,137 @@ mod tests { assert_eq!(right.len(), 3); // base => b_ase }); } + + #[test] + fn should_replace_gc_item_with_content_deleted() { + loom_model!({ + let item1 = ItemBuilder::new() + .id((1, 0).into()) + .content(Content::String(String::from("octo"))) + .build(); + let item2 = ItemBuilder::new() + .id((1, 4).into()) + .content(Content::String(String::from("base"))) + .build(); + + item1.delete(); + + let mut store = DocStore::with_client(1); + + store.add_node(Node::Item(Somr::new(item1))).unwrap(); + store.add_node(Node::Item(Somr::new(item2))).unwrap(); + store.delete_set.add_range(1, 0..4); + + store.gc_delete_set().unwrap(); + + assert_eq!( + store + .get_node((1, 0)) + .unwrap() + .as_item() + .get() + .unwrap() + .content + .as_ref(), + &Content::Deleted(4) + ); + }); + } + + #[test] + fn should_gc_type_items() { + loom_model!({ + let doc = DocOptions::new().with_client_id(1).build(); + + let mut arr = doc.get_or_create_array("arr").unwrap(); + let mut text = doc.create_text().unwrap(); + + arr.insert(0, Value::from(text.clone())).unwrap(); + + text.insert(0, "hello world").unwrap(); + text.remove(5, 6).unwrap(); + + arr.remove(0, 1).unwrap(); + let mut store = doc.store.write().unwrap(); + store.gc_delete_set().unwrap(); + + assert_eq!(arr.len(), 0); + assert_eq!( + store + .get_node((1, 0)) + .unwrap() + .as_item() + .get() + .unwrap() + .content + .as_ref(), + &Content::Deleted(1) + ); + + assert_eq!( + store.get_node((1, 1)).unwrap(), // "hello" GCd + Node::new_gc((1, 1).into(), 5) + ); + + assert_eq!( + store.get_node((1, 7)).unwrap(), // " world" GCd + Node::new_gc((1, 6).into(), 6) + ); + }); + } + + #[test] + fn should_merge_same_sibling_items() { + loom_model!({ + let mut store = DocStore::with_client(1); + store.items.insert( + 1, + VecDeque::from([ + Node::new_gc((1, 0).into(), 2), + Node::new_gc((1, 2).into(), 2), + Node::new_skip((1, 4).into(), 2), + Node::Item(Somr::new( + ItemBuilder::new() + .id((1, 6).into()) + .content(Content::String(String::from("hello"))) + .build(), + )), + // actually not mergable, due to runtime continuous check + // will cover it in [test_merge_same_sibling_items2] + Node::Item(Somr::new( + ItemBuilder::new() + .id((1, 11).into()) + .content(Content::String(String::from("world"))) + .left_id(Some((1, 11).into())) + .build(), + )), + ]), + ); + + store.make_continuous().unwrap(); + + assert_eq!(store.items.get(&1).unwrap().len(), 4); + }); + } + + #[test] + fn test_merge_same_sibling_items2() { + loom_model!({ + let doc = Doc::new(); + + let mut text = doc.get_or_create_text("text").unwrap(); + text.insert(0, "a").unwrap(); + text.insert(1, "b").unwrap(); + text.insert(2, "c").unwrap(); + text.insert(3, ", hello").unwrap(); + + assert_eq!(text.to_string(), "abc, hello"); + + let mut store = doc.store.write().unwrap(); + assert_eq!(store.items.get(&1).unwrap().len(), 4); + store.make_continuous().unwrap(); + assert_eq!(store.items.get(&1).unwrap().len(), 1); + assert_eq!(text.to_string(), "abc, hello"); + }); + } } diff --git a/y-octo/src/doc/types/array.rs b/y-octo/src/doc/types/array.rs index 83859ef..f68123d 100644 --- a/y-octo/src/doc/types/array.rs +++ b/y-octo/src/doc/types/array.rs @@ -89,10 +89,7 @@ mod tests { #[test] fn test_yarray_insert() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; + let options = DocOptions::default(); loom_model!({ let doc = Doc::with_options(options.clone()); @@ -111,15 +108,8 @@ mod tests { #[test] #[cfg_attr(miri, ignore)] fn test_ytext_equal() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - let yrs_options = Options { - client_id: rand::random(), - guid: nanoid::nanoid!().into(), - ..Default::default() - }; + let options = DocOptions::default(); + let yrs_options = Options::default(); loom_model!({ let doc = yrs::Doc::with_options(yrs_options.clone()); @@ -145,15 +135,9 @@ mod tests { assert_eq!(array.get(11).unwrap(), Value::Any(Any::String("!".into()))); }); - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - let yrs_options = Options { - client_id: rand::random(), - guid: nanoid::nanoid!().into(), - ..Default::default() - }; + let options = DocOptions::default(); + let yrs_options = Options::default(); + loom_model!({ let doc = yrs::Doc::with_options(yrs_options.clone()); let array = doc.get_or_insert_text("abc"); @@ -198,8 +182,9 @@ mod tests { let doc = Doc::new_from_binary_with_options( update.clone(), DocOptions { - guid: Some(String::from("1")), - client: Some(1), + guid: String::from("1"), + client_id: 1, + gc: true, }, ) .unwrap(); diff --git a/y-octo/src/doc/types/list/mod.rs b/y-octo/src/doc/types/list/mod.rs index 15238ee..edf340d 100644 --- a/y-octo/src/doc/types/list/mod.rs +++ b/y-octo/src/doc/types/list/mod.rs @@ -5,7 +5,6 @@ pub(crate) use iterator::ListIterator; pub use search_marker::MarkerList; use super::*; -use crate::sync::RwLockWriteGuard; pub(crate) struct ItemPosition { pub parent: YTypeRef, @@ -58,11 +57,11 @@ impl ItemPosition { pub(crate) trait ListType: AsInner { #[inline(always)] fn content_len(&self) -> u64 { - self.as_inner().get().unwrap().read().unwrap().len + self.as_inner().ty().unwrap().len } fn iter_item(&self) -> ListIterator { - let inner = self.as_inner().get().unwrap().read().unwrap(); + let inner = self.as_inner().ty().unwrap(); ListIterator::new(inner.start.clone()) } @@ -123,13 +122,10 @@ pub(crate) trait ListType: AsInner { return Err(JwstCodecError::IndexOutOfBound(index)); } - if let Some(ty) = self.as_inner().get() { - let inner = ty.write().unwrap(); - if let Some(mut pos) = self.find_pos(&inner, index) { - if let Some(mut store) = inner.store_mut() { - pos.normalize(&mut store)?; - Self::insert_after(inner, &mut store, pos, content)?; - } + if let Some((mut store, mut ty)) = self.as_inner().write() { + if let Some(mut pos) = self.find_pos(&ty, index) { + pos.normalize(&mut store)?; + Self::insert_after(&mut ty, &mut store, pos, content)?; } } else { return Err(JwstCodecError::DocReleased); @@ -138,13 +134,8 @@ pub(crate) trait ListType: AsInner { Ok(()) } - fn insert_after( - mut lock: RwLockWriteGuard, - store: &mut DocStore, - pos: ItemPosition, - content: Content, - ) -> JwstCodecResult { - if let Some(markers) = &lock.markers { + fn insert_after(ty: &mut YType, store: &mut DocStore, pos: ItemPosition, content: Content) -> JwstCodecResult { + if let Some(markers) = &ty.markers { markers.update_marker_changes(pos.index, content.clock_len() as i64); } @@ -156,7 +147,7 @@ pub(crate) trait ListType: AsInner { None, ); - store.integrate(Node::Item(item), 0, Some(&mut lock))?; + store.integrate(Node::Item(item), 0, Some(ty))?; Ok(()) } @@ -166,9 +157,9 @@ pub(crate) trait ListType: AsInner { return None; } - let inner = self.as_inner().get().unwrap().read().unwrap(); + let ty = self.as_inner().ty().unwrap(); - if let Some(pos) = self.find_pos(&inner, index) { + if let Some(pos) = self.find_pos(&ty, index) { if pos.offset == 0 { return Some((pos.right, 0)); } else { @@ -188,12 +179,9 @@ pub(crate) trait ListType: AsInner { return Err(JwstCodecError::IndexOutOfBound(idx)); } - if let Some(ty) = self.as_inner().get() { - let inner = ty.write().unwrap(); - if let Some(pos) = self.find_pos(&inner, idx) { - if let Some(mut store) = inner.store_mut() { - Self::remove_after(inner, &mut store, pos, len)?; - } + if let Some((mut store, mut ty)) = self.as_inner().write() { + if let Some(pos) = self.find_pos(&ty, idx) { + Self::remove_after(&mut ty, &mut store, pos, len)?; } } else { return Err(JwstCodecError::DocReleased); @@ -202,12 +190,7 @@ pub(crate) trait ListType: AsInner { Ok(()) } - fn remove_after( - mut lock: RwLockWriteGuard, - store: &mut DocStore, - mut pos: ItemPosition, - len: u64, - ) -> JwstCodecResult { + fn remove_after(ty: &mut YType, store: &mut DocStore, mut pos: ItemPosition, len: u64) -> JwstCodecResult { pos.normalize(store)?; let mut remaining = len; @@ -222,8 +205,8 @@ pub(crate) trait ListType: AsInner { } else { remaining -= content_len; } - store.delete_set.add(item.id.client, item.id.clock, content_len); - DocStore::delete_item(item, Some(&mut lock)); + + store.delete_item(item, Some(ty)); } pos.forward(); @@ -232,7 +215,7 @@ pub(crate) trait ListType: AsInner { } } - if let Some(markers) = &lock.markers { + if let Some(markers) = &ty.markers { markers.update_marker_changes(pos.index, -((len - remaining) as i64)); } diff --git a/y-octo/src/doc/types/list/search_marker.rs b/y-octo/src/doc/types/list/search_marker.rs index 6466bbc..e25231c 100644 --- a/y-octo/src/doc/types/list/search_marker.rs +++ b/y-octo/src/doc/types/list/search_marker.rs @@ -216,15 +216,8 @@ mod tests { #[test] fn test_marker_list() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - let yrs_options = Options { - client_id: rand::random(), - guid: nanoid::nanoid!().into(), - ..Default::default() - }; + let options = DocOptions::default(); + let yrs_options = Options::default(); loom_model!({ let (client_id, buffer) = if cfg!(miri) { @@ -257,7 +250,7 @@ mod tests { let marker_list = MarkerList::new(); - let marker = marker_list.find_marker(&array.read().unwrap(), 8).unwrap(); + let marker = marker_list.find_marker(&array.0.ty().unwrap(), 8).unwrap(); assert_eq!(marker.index, 2); assert_eq!( @@ -274,10 +267,7 @@ mod tests { #[test] fn test_search_marker_flaky() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; + let options = DocOptions::default(); loom_model!({ let doc = Doc::with_options(options.clone()); diff --git a/y-octo/src/doc/types/map.rs b/y-octo/src/doc/types/map.rs index 5044a48..ab40543 100644 --- a/y-octo/src/doc/types/map.rs +++ b/y-octo/src/doc/types/map.rs @@ -12,14 +12,9 @@ impl_type!(Map); pub(crate) trait MapType: AsInner { fn insert(&mut self, key: impl AsRef, value: impl Into) -> JwstCodecResult { - let mut inner = self.as_inner().get().unwrap().write().unwrap(); - let left = inner.map.as_ref().and_then(|map| { - map.get(key.as_ref()) - .and_then(|struct_info| struct_info.left()) - .map(|l| l.as_item()) - }); - if let Some(store) = inner.store.upgrade() { - let mut store = store.write().unwrap(); + if let Some((mut store, mut ty)) = self.as_inner().write() { + let left = ty.map.as_ref().and_then(|map| map.get(key.as_ref())).cloned(); + let item = store.create_item( value.into(), left.unwrap_or(Somr::none()), @@ -27,36 +22,40 @@ pub(crate) trait MapType: AsInner { Some(Parent::Type(self.as_inner().clone())), Some(key.as_ref().into()), ); - store.integrate(Node::Item(item), 0, Some(&mut inner))?; + store.integrate(Node::Item(item), 0, Some(&mut ty))?; } Ok(()) } fn keys(&self) -> Vec { - let inner = self.as_inner().get().unwrap().read().unwrap(); - inner - .map - .as_ref() - .map_or(Vec::new(), |map| map.keys().cloned().collect()) + if let Some(ty) = self.as_inner().ty() { + ty.map.as_ref().map_or(Vec::new(), |map| map.keys().cloned().collect()) + } else { + vec![] + } } fn get(&self, key: impl AsRef) -> Option> { - let inner = self.as_inner().get().unwrap().read().unwrap(); - inner - .map - .as_ref() - .and_then(|map| map.get(key.as_ref())) - .filter(|struct_info| !struct_info.deleted()) - .map(|struct_info| struct_info.as_item().get().unwrap().content.clone()) + self.as_inner().ty().and_then(|ty| { + ty.map + .as_ref() + .and_then(|map| map.get(key.as_ref())) + .filter(|item| item.get().map(|item| !item.deleted()).unwrap_or(false)) + .map(|item| item.get().unwrap().content.clone()) + }) } - fn get_all(&self) -> HashMap>> { + fn get_all(&self) -> HashMap> { let mut ret = HashMap::default(); - let inner = self.as_inner().get().unwrap().read().unwrap(); - if let Some(map) = inner.map.as_ref() { - for key in map.keys() { - ret.insert(key.clone(), self.get(key)); + + if let Some(ty) = self.as_inner().ty() { + if let Some(map) = ty.map.as_ref() { + for key in map.keys() { + if let Some(content) = self.get(key) { + ret.insert(key.clone(), content); + } + } } } @@ -64,54 +63,61 @@ pub(crate) trait MapType: AsInner { } fn contains_key(&self, key: impl AsRef) -> bool { - let inner = self.as_inner().get().unwrap().read().unwrap(); - inner - .map - .as_ref() - .and_then(|map| map.get(key.as_ref())) - .map(|struct_info| !struct_info.deleted()) - .unwrap_or(false) + if let Some(ty) = self.as_inner().ty() { + ty.map + .as_ref() + .and_then(|map| map.get(key.as_ref())) + .and_then(|item| item.get()) + .map_or(false, |item| !item.deleted()) + } else { + false + } } fn remove(&mut self, key: impl AsRef) -> bool { - let mut inner = self.as_inner().get().unwrap().write().unwrap(); - let node = inner.map.as_ref().and_then(|map| map.get(key.as_ref())); - if let Some(store) = inner.store.upgrade() { - let mut store = store.write().unwrap(); - if let Some(item) = ItemRef::from(node).get() { - store.delete_set.add(item.id.client, item.id.clock, item.len()); - DocStore::delete_item(item, Some(&mut inner)); - return true; + if let Some((mut store, mut ty)) = self.as_inner().write() { + let node = ty.map.as_ref().and_then(|map| map.get(key.as_ref())); + if let Some(node) = node { + if let Some(item) = node.clone().get() { + store.delete_item(item, Some(&mut ty)); + return true; + } } } + false } fn len(&self) -> u64 { - let inner = self.as_inner().get().unwrap().write().unwrap(); - inner - .map - .as_ref() - .map_or(0, |map| map.values().filter(|v| !v.deleted()).count()) as u64 + self.as_inner() + .ty() + .map(|ty| { + ty.map.as_ref().map_or(0, |map| { + map.values() + .filter(|v| v.get().map(|item| !item.deleted()).unwrap_or(false)) + .count() + }) as u64 + }) + .unwrap_or(0) } fn iter(&self) -> MapIterator { - let inner = self.as_inner().get().unwrap().read().unwrap(); + let inner = self.as_inner().ty().unwrap(); let map = inner.map.as_ref().map(|map| { map.iter() .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() + .collect::)>>() }); MapIterator { - nodes: map.unwrap_or(vec![]), + nodes: map.unwrap_or_default(), index: 0, } } } pub struct MapIterator { - pub(super) nodes: Vec<(String, Node)>, + pub(super) nodes: Vec<(String, Somr)>, pub(super) index: usize, } @@ -126,15 +132,13 @@ impl Iterator for MapIterator { while self.index < len { let (name, node) = self.nodes[self.index].clone(); - self.index += 1; - if node.deleted() { - continue; - } + if let Some(item) = node.get() { + self.index += 1; + if item.deleted() { + continue; + } - if let Some(item) = node.as_item().get() { return item.content.as_ref().try_into().ok().map(|item| (name, item)); - } else { - continue; } } @@ -208,13 +212,8 @@ mod tests { #[test] fn test_map_basic() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut map = doc.get_or_create_map("map").unwrap(); map.insert("1", "value").unwrap(); assert_eq!(map.get("1").unwrap(), Value::Any(Any::String("value".to_string()))); @@ -229,19 +228,14 @@ mod tests { #[test] fn test_map_equal() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut map = doc.get_or_create_map("map").unwrap(); map.insert("1", "value").unwrap(); map.insert("2", false).unwrap(); let binary = doc.encode_update_v1().unwrap(); - let new_doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap(); + let new_doc = Doc::new_from_binary(binary).unwrap(); let map = new_doc.get_or_create_map("map").unwrap(); assert_eq!(map.get("1").unwrap(), Value::Any(Any::String("value".to_string()))); assert_eq!(map.get("2").unwrap(), Value::Any(Any::False)); @@ -249,6 +243,18 @@ mod tests { }); } + #[test] + fn test_map_renew_value() { + loom_model!({ + let doc = Doc::new(); + let mut map = doc.get_or_create_map("map").unwrap(); + map.insert("1", "value").unwrap(); + map.insert("1", "value2").unwrap(); + assert_eq!(map.get("1").unwrap(), Value::Any(Any::String("value2".to_string()))); + assert_eq!(map.len(), 1); + }); + } + // #[test] // fn test_map_iter() { // let options = DocOptions { @@ -280,14 +286,9 @@ mod tests { #[test] fn test_map_re_encode() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ let binary = { - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut map = doc.get_or_create_map("map").unwrap(); map.insert("1", "value1").unwrap(); map.insert("2", "value2").unwrap(); @@ -295,7 +296,7 @@ mod tests { }; { - let doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap(); + let doc = Doc::new_from_binary(binary).unwrap(); let map = doc.get_or_create_map("map").unwrap(); assert_eq!(map.get("1").unwrap(), Value::Any(Any::String("value1".to_string()))); assert_eq!(map.get("2").unwrap(), Value::Any(Any::String("value2".to_string()))); diff --git a/y-octo/src/doc/types/mod.rs b/y-octo/src/doc/types/mod.rs index 6b12497..3b29ee2 100644 --- a/y-octo/src/doc/types/mod.rs +++ b/y-octo/src/doc/types/mod.rs @@ -3,7 +3,10 @@ mod list; mod map; mod text; -use std::collections::{hash_map::Entry, HashMap}; +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Weak, +}; pub use array::*; use list::*; @@ -21,21 +24,23 @@ use crate::{ #[derive(Debug, Default)] pub(crate) struct YType { - pub store: WeakStoreRef, pub start: Somr, pub item: Somr, - pub map: Option>, + pub map: Option>>, pub len: u64, /// The tag name of XMLElement and XMLHook type pub name: Option, /// The name of the type that directly belongs the store. pub root_name: Option, - pub kind: YTypeKind, + kind: YTypeKind, pub markers: Option, } -type Lock = RwLock; -pub(crate) type YTypeRef = Somr; +#[derive(Debug, Default, Clone)] +pub(crate) struct YTypeRef { + pub store: WeakStoreRef, + pub inner: Somr>, +} impl PartialEq for YType { fn eq(&self, other: &Self) -> bool { @@ -45,6 +50,17 @@ impl PartialEq for YType { } } +impl PartialEq for YTypeRef { + fn eq(&self, other: &Self) -> bool { + self.inner.ptr_eq(&other.inner) + || match (self.ty(), other.ty()) { + (Some(l), Some(r)) => *l == *r, + (None, None) => true, + _ => false, + } + } +} + impl YType { pub fn new(kind: YTypeKind, tag_name: Option) -> Self { YType { @@ -71,6 +87,23 @@ impl YType { Ok(()) } +} + +impl YTypeRef { + pub fn new(kind: YTypeKind, tag_name: Option) -> Self { + Self { + inner: Somr::new(RwLock::new(YType::new(kind, tag_name))), + store: Weak::new(), + } + } + + pub fn ty(&self) -> Option> { + self.inner.get().and_then(|ty| ty.read().ok()) + } + + pub fn ty_mut(&self) -> Option> { + self.inner.get().and_then(|ty| ty.write().ok()) + } #[allow(dead_code)] pub fn store<'a>(&self) -> Option> { @@ -92,6 +125,15 @@ impl YType { None } } + + #[allow(dead_code)] + pub fn read(&self) -> Option<(RwLockReadGuard, RwLockReadGuard)> { + self.store().and_then(|store| self.ty().map(|ty| (store, ty))) + } + + pub fn write(&self) -> Option<(RwLockWriteGuard, RwLockWriteGuard)> { + self.store_mut().and_then(|store| self.ty_mut().map(|ty| (store, ty))) + } } pub(crate) struct YTypeBuilder { @@ -156,34 +198,42 @@ impl YTypeBuilder { match store.types.entry(root_name.clone()) { Entry::Occupied(e) => e.get().clone(), Entry::Vacant(e) => { - let ty = Somr::new(RwLock::new(YType { + let inner = Somr::new(RwLock::new(YType { kind: self.kind, name: self.name, root_name: Some(root_name), - store: Arc::downgrade(&self.store), markers: Self::markers(self.kind), ..Default::default() })); - let ty_ref = ty.clone(); + let ty = YTypeRef { + store: Arc::downgrade(&self.store), + inner, + }; + let ty_ref = ty.clone(); e.insert(ty); ty_ref } } } else { - let ty = Somr::new(RwLock::new(YType { + let inner = Somr::new(RwLock::new(YType { kind: self.kind, name: self.name, root_name: self.root_name.clone(), - store: Arc::downgrade(&self.store), markers: Self::markers(self.kind), - ..YType::default() + ..Default::default() })); + + let ty = YTypeRef { + store: Arc::downgrade(&self.store), + inner, + }; + let ty_ref = ty.clone(); - store.dangling_types.insert(ty.ptr().as_ptr() as usize, ty); + store.dangling_types.insert(ty.inner.ptr().as_ptr() as usize, ty); ty_ref }; @@ -239,15 +289,6 @@ macro_rules! impl_variants { } } } - - - $( - impl From<$name> for super::Value { - fn from(value: $name) -> Self { - Self::$name(value) - } - } - )* }; } @@ -259,7 +300,7 @@ pub(crate) trait AsInner { #[macro_export(local_inner_macros)] macro_rules! impl_type { ($name: ident) => { - #[derive(Debug, Clone)] + #[derive(Debug, Clone, PartialEq)] pub struct $name(pub(crate) super::YTypeRef); unsafe impl Sync for $name {} unsafe impl Send for $name {} @@ -268,26 +309,6 @@ macro_rules! impl_type { pub(crate) fn new(inner: super::YTypeRef) -> Self { Self(inner) } - - #[allow(dead_code)] - #[inline(always)] - pub(crate) fn read(&self) -> $crate::JwstCodecResult<$crate::sync::RwLockReadGuard> { - if let Some(lock) = self.0.get() { - Ok(lock.read().unwrap()) - } else { - Err($crate::JwstCodecError::DocReleased) - } - } - - #[allow(dead_code)] - #[inline(always)] - pub(crate) fn write(&self) -> $crate::JwstCodecResult<$crate::sync::RwLockWriteGuard> { - if let Some(lock) = self.0.get() { - Ok(lock.write().unwrap()) - } else { - Err($crate::JwstCodecError::DocReleased) - } - } } impl super::AsInner for $name { @@ -303,14 +324,17 @@ macro_rules! impl_type { type Error = $crate::JwstCodecError; fn try_from(value: super::YTypeRef) -> Result { - let mut inner = value.get().unwrap().write().unwrap(); - match inner.kind { - super::YTypeKind::$name => Ok($name::new(value.clone())), - super::YTypeKind::Unknown => { - inner.set_kind(super::YTypeKind::$name)?; - Ok($name::new(value.clone())) + if let Some((_, mut inner)) = value.write() { + match inner.kind { + super::YTypeKind::$name => Ok($name::new(value.clone())), + super::YTypeKind::Unknown => { + inner.set_kind(super::YTypeKind::$name)?; + Ok($name::new(value.clone())) + } + _ => Err($crate::JwstCodecError::TypeCastError(std::stringify!($name))), } - _ => Err($crate::JwstCodecError::TypeCastError(std::stringify!($name))), + } else { + Err($crate::JwstCodecError::TypeCastError(std::stringify!($name))) } } } @@ -321,9 +345,9 @@ macro_rules! impl_type { } } - impl PartialEq for $name { - fn eq(&self, other: &Self) -> bool { - &*self.read().unwrap() == &*other.read().unwrap() + impl From<$name> for super::Value { + fn from(value: $name) -> Self { + Self::$name(value) } } }; @@ -416,7 +440,7 @@ impl TryFrom<&Content> for Value { )), Content::Binary(buf) => Value::Any(Any::Binary(buf.clone())), Content::Embed(v) => Value::Any(v.clone()), - Content::Type(ty) => match ty.get().unwrap().read().unwrap().kind { + Content::Type(ty) => match ty.ty().unwrap().kind { YTypeKind::Array => Value::Array(Array::from_unchecked(ty.clone())), YTypeKind::Map => Value::Map(Map::from_unchecked(ty.clone())), YTypeKind::Text => Value::Text(Text::from_unchecked(ty.clone())), @@ -427,7 +451,7 @@ impl TryFrom<&Content> for Value { // actually unreachable YTypeKind::Unknown => return Err(JwstCodecError::TypeCastError("unknown")), }, - Content::Doc { .. } => return Err(JwstCodecError::TypeCastError("unimplemented: Doc")), + Content::Doc { guid: _, opts } => Value::Doc(DocOptions::try_from(opts.clone())?.build()), Content::Format { .. } => return Err(JwstCodecError::TypeCastError("unimplemented: Format")), Content::Deleted(_) => return Err(JwstCodecError::TypeCastError("unimplemented: Deleted")), }) @@ -440,8 +464,7 @@ impl From for Content { Value::Any(any) => Content::from(any), Value::Doc(doc) => Content::Doc { guid: doc.guid().to_owned(), - // TODO: replace doc options if we got ones - opts: Any::Undefined, + opts: Any::from(doc.options().clone()), }, Value::Array(v) => Content::Type(v.0), Value::Map(v) => Content::Type(v.0), diff --git a/y-octo/src/doc/types/text.rs b/y-octo/src/doc/types/text.rs index f8733ef..275f1f8 100644 --- a/y-octo/src/doc/types/text.rs +++ b/y-octo/src/doc/types/text.rs @@ -61,18 +61,13 @@ mod tests { use crate::{ loom_model, sync::{thread, Arc, AtomicUsize, Ordering}, - Doc, DocOptions, + Doc, }; #[test] fn test_manipulate_text() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut text = doc.create_text().unwrap(); text.insert(0, "llo").unwrap(); @@ -205,17 +200,12 @@ mod tests { #[test] fn loom_parallel_ins_del_text() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - let seed = rand::thread_rng().gen(); let mut rand = ChaCha20Rng::seed_from_u64(seed); let ranges = (0..20).map(|_| rand.gen_range(0..16)).collect::>(); loom_model!({ - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut text = doc.get_or_create_text("test").unwrap(); text.insert(0, "This is a string with length 32.").unwrap(); @@ -264,14 +254,9 @@ mod tests { trx.encode_update_v1().unwrap() }; - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ let binary = binary.clone(); - let doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap(); + let doc = Doc::new_from_binary(binary).unwrap(); let mut text = doc.get_or_create_text("greating").unwrap(); assert_eq!(text.to_string(), "hello world"); @@ -284,14 +269,9 @@ mod tests { #[test] fn test_recover_from_octobase_encoder() { - let options = DocOptions { - client: Some(rand::random()), - guid: Some(nanoid::nanoid!()), - }; - loom_model!({ let binary = { - let doc = Doc::with_options(options.clone()); + let doc = Doc::new(); let mut text = doc.get_or_create_text("greating").unwrap(); text.insert(0, "hello").unwrap(); text.insert(5, " world!").unwrap(); @@ -301,7 +281,7 @@ mod tests { }; let binary = binary.clone(); - let doc = Doc::new_from_binary_with_options(binary, options.clone()).unwrap(); + let doc = Doc::new_from_binary(binary).unwrap(); let mut text = doc.get_or_create_text("greating").unwrap(); assert_eq!(text.to_string(), "hello world"); diff --git a/y-octo/src/lib.rs b/y-octo/src/lib.rs index 48bda57..407131f 100644 --- a/y-octo/src/lib.rs +++ b/y-octo/src/lib.rs @@ -12,7 +12,6 @@ pub use doc::{ }; pub(crate) use doc::{Content, Item}; use log::{debug, warn}; -use nanoid::nanoid; use nom::IResult; pub use protocol::{ read_sync_message, write_sync_message, AwarenessState, AwarenessStates, DocMessage, SyncMessage, SyncMessageScanner, @@ -21,6 +20,8 @@ use thiserror::Error; #[derive(Debug, Error, PartialEq)] pub enum JwstCodecError { + #[error("Unexpected Scenario")] + Unexpected, #[error("Damaged document: corrupt json data")] DamagedDocumentJson, #[error("Incomplete document: {0}")] @@ -57,6 +58,8 @@ pub enum JwstCodecError { IndexOutOfBound(u64), #[error("Document has been released")] DocReleased, + #[error("Unexpected type, expect {0}")] + UnexpectedType(&'static str), } pub type JwstCodecResult = Result; diff --git a/yarn.lock b/yarn.lock index 647cc9e..8c73dbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,13 @@ __metadata: languageName: node linkType: hard +"@nolyfill/shared@npm:1.0.20": + version: 1.0.20 + resolution: "@nolyfill/shared@npm:1.0.20" + checksum: cb2511c301e0e5dc15926a59a396352d0dd7f3376979c9526538ee25e223bc1d4524ecca4d245c1cd0c377d47ddbcad7f606d151f382b00f47844ef5f61872e5 + languageName: node + linkType: hard + "@taplo/cli@npm:^0.5.2": version: 0.5.2 resolution: "@taplo/cli@npm:0.5.2" @@ -245,37 +252,6 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.0": - version: 1.0.0 - resolution: "array-buffer-byte-length@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - is-array-buffer: ^3.0.1 - checksum: 044e101ce150f4804ad19c51d6c4d4cfa505c5b2577bd179256e4aa3f3f6a0a5e9874c78cd428ee566ac574c8a04d7ce21af9fe52e844abfdccb82b33035a7c3 - languageName: node - linkType: hard - -"arraybuffer.prototype.slice@npm:^1.0.1": - version: 1.0.1 - resolution: "arraybuffer.prototype.slice@npm:1.0.1" - dependencies: - array-buffer-byte-length: ^1.0.0 - call-bind: ^1.0.2 - define-properties: ^1.2.0 - get-intrinsic: ^1.2.1 - is-array-buffer: ^3.0.2 - is-shared-array-buffer: ^1.0.2 - checksum: e3e9b2a3e988ebfeddce4c7e8f69df730c9e48cb04b0d40ff0874ce3d86b3d1339dd520ffde5e39c02610bc172ecfbd4bc93324b1cabd9554c44a56b131ce0ce - languageName: node - linkType: hard - -"available-typed-arrays@npm:^1.0.5": - version: 1.0.5 - resolution: "available-typed-arrays@npm:1.0.5" - checksum: 20eb47b3cefd7db027b9bbb993c658abd36d4edd3fe1060e83699a03ee275b0c9b216cc076ff3f2db29073225fb70e7613987af14269ac1fe2a19803ccc97f1a - languageName: node - linkType: hard - "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -324,16 +300,6 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind@npm:1.0.2" - dependencies: - function-bind: ^1.1.1 - get-intrinsic: ^1.0.2 - checksum: f8e31de9d19988a4b80f3e704788c4a2d6b6f3d17cfec4f57dc29ced450c53a49270dc66bf0fbd693329ee948dd33e6c90a329519aef17474a4d961e8d6426b0 - languageName: node - linkType: hard - "chalk@npm:5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -485,16 +451,6 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0": - version: 1.2.0 - resolution: "define-properties@npm:1.2.0" - dependencies: - has-property-descriptors: ^1.0.0 - object-keys: ^1.1.1 - checksum: e60aee6a19b102df4e2b1f301816804e81ab48bb91f00d0d935f269bf4b3f79c88b39e4f89eaa132890d23267335fd1140dfcd8d5ccd61031a0a2c41a54e33a6 - languageName: node - linkType: hard - "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -532,75 +488,6 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4": - version: 1.22.1 - resolution: "es-abstract@npm:1.22.1" - dependencies: - array-buffer-byte-length: ^1.0.0 - arraybuffer.prototype.slice: ^1.0.1 - available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 - es-set-tostringtag: ^2.0.1 - es-to-primitive: ^1.2.1 - function.prototype.name: ^1.1.5 - get-intrinsic: ^1.2.1 - get-symbol-description: ^1.0.0 - globalthis: ^1.0.3 - gopd: ^1.0.1 - has: ^1.0.3 - has-property-descriptors: ^1.0.0 - has-proto: ^1.0.1 - has-symbols: ^1.0.3 - internal-slot: ^1.0.5 - is-array-buffer: ^3.0.2 - is-callable: ^1.2.7 - is-negative-zero: ^2.0.2 - is-regex: ^1.1.4 - is-shared-array-buffer: ^1.0.2 - is-string: ^1.0.7 - is-typed-array: ^1.1.10 - is-weakref: ^1.0.2 - object-inspect: ^1.12.3 - object-keys: ^1.1.1 - object.assign: ^4.1.4 - regexp.prototype.flags: ^1.5.0 - safe-array-concat: ^1.0.0 - safe-regex-test: ^1.0.0 - string.prototype.trim: ^1.2.7 - string.prototype.trimend: ^1.0.6 - string.prototype.trimstart: ^1.0.6 - typed-array-buffer: ^1.0.0 - typed-array-byte-length: ^1.0.0 - typed-array-byte-offset: ^1.0.0 - typed-array-length: ^1.0.4 - unbox-primitive: ^1.0.2 - which-typed-array: ^1.1.10 - checksum: 614e2c1c3717cb8d30b6128ef12ea110e06fd7d75ad77091ca1c5dbfb00da130e62e4bbbbbdda190eada098a22b27fe0f99ae5a1171dac2c8663b1e8be8a3a9b - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.0.1": - version: 2.0.1 - resolution: "es-set-tostringtag@npm:2.0.1" - dependencies: - get-intrinsic: ^1.1.3 - has: ^1.0.3 - has-tostringtag: ^1.0.0 - checksum: ec416a12948cefb4b2a5932e62093a7cf36ddc3efd58d6c58ca7ae7064475ace556434b869b0bbeb0c365f1032a8ccd577211101234b69837ad83ad204fff884 - languageName: node - linkType: hard - -"es-to-primitive@npm:^1.2.1": - version: 1.2.1 - resolution: "es-to-primitive@npm:1.2.1" - dependencies: - is-callable: ^1.1.4 - is-date-object: ^1.0.1 - is-symbol: ^1.0.2 - checksum: 4ead6671a2c1402619bdd77f3503991232ca15e17e46222b0a41a5d81aebc8740a77822f5b3c965008e631153e9ef0580540007744521e72de8e33599fca2eed - languageName: node - linkType: hard - "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -658,15 +545,6 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" - dependencies: - is-callable: ^1.1.3 - checksum: 6c48ff2bc63362319c65e2edca4a8e1e3483a2fabc72fbe7feaf8c73db94fc7861bd53bc02c8a66a0c1dd709da6b04eec42e0abdd6b40ce47305ae92a25e5d28 - languageName: node - linkType: hard - "foreground-child@npm:^2.0.0": version: 2.0.0 resolution: "foreground-child@npm:2.0.0" @@ -684,32 +562,6 @@ __metadata: languageName: node linkType: hard -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a - languageName: node - linkType: hard - -"function.prototype.name@npm:^1.1.5": - version: 1.1.5 - resolution: "function.prototype.name@npm:1.1.5" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.3 - es-abstract: ^1.19.0 - functions-have-names: ^1.2.2 - checksum: acd21d733a9b649c2c442f067567743214af5fa248dbeee69d8278ce7df3329ea5abac572be9f7470b4ec1cd4d8f1040e3c5caccf98ebf2bf861a0deab735c27 - languageName: node - linkType: hard - -"functions-have-names@npm:^1.2.2, functions-have-names@npm:^1.2.3": - version: 1.2.3 - resolution: "functions-have-names@npm:1.2.3" - checksum: c3f1f5ba20f4e962efb71344ce0a40722163e85bee2101ce25f88214e78182d2d2476aa85ef37950c579eb6cf6ee811c17b3101bb84004bb75655f3e33f3fdb5 - languageName: node - linkType: hard - "get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" @@ -717,18 +569,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": - version: 1.2.1 - resolution: "get-intrinsic@npm:1.2.1" - dependencies: - function-bind: ^1.1.1 - has: ^1.0.3 - has-proto: ^1.0.1 - has-symbols: ^1.0.3 - checksum: 5b61d88552c24b0cf6fa2d1b3bc5459d7306f699de060d76442cce49a4721f52b8c560a33ab392cf5575b7810277d54ded9d4d39a1ea61855619ebc005aa7e5f - languageName: node - linkType: hard - "get-stream@npm:^6.0.1": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -736,16 +576,6 @@ __metadata: languageName: node linkType: hard -"get-symbol-description@npm:^1.0.0": - version: 1.0.0 - resolution: "get-symbol-description@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.1.1 - checksum: 9ceff8fe968f9270a37a1f73bf3f1f7bda69ca80f4f80850670e0e7b9444ff99323f7ac52f96567f8b5f5fbe7ac717a0d81d3407c7313e82810c6199446a5247 - languageName: node - linkType: hard - "glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -760,24 +590,6 @@ __metadata: languageName: node linkType: hard -"globalthis@npm:^1.0.3": - version: 1.0.3 - resolution: "globalthis@npm:1.0.3" - dependencies: - define-properties: ^1.1.3 - checksum: fbd7d760dc464c886d0196166d92e5ffb4c84d0730846d6621a39fbbc068aeeb9c8d1421ad330e94b7bca4bb4ea092f5f21f3d36077812af5d098b4dc006c998 - languageName: node - linkType: hard - -"gopd@npm:^1.0.1": - version: 1.0.1 - resolution: "gopd@npm:1.0.1" - dependencies: - get-intrinsic: ^1.1.3 - checksum: a5ccfb8806e0917a94e0b3de2af2ea4979c1da920bc381667c260e00e7cafdbe844e2cb9c5bcfef4e5412e8bf73bab837285bc35c7ba73aaaf0134d4583393a6 - languageName: node - linkType: hard - "graceful-fs@npm:^4.1.2": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -785,13 +597,6 @@ __metadata: languageName: node linkType: hard -"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": - version: 1.0.2 - resolution: "has-bigints@npm:1.0.2" - checksum: 390e31e7be7e5c6fe68b81babb73dfc35d413604d7ee5f56da101417027a4b4ce6a27e46eff97ad040c835b5d228676eae99a9b5c3bc0e23c8e81a49241ff45b - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -806,44 +611,12 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0": - version: 1.0.0 - resolution: "has-property-descriptors@npm:1.0.0" +"has@npm:@nolyfill/has@latest": + version: 1.0.20 + resolution: "@nolyfill/has@npm:1.0.20" dependencies: - get-intrinsic: ^1.1.1 - checksum: a6d3f0a266d0294d972e354782e872e2fe1b6495b321e6ef678c9b7a06a40408a6891817350c62e752adced73a94ac903c54734fee05bf65b1905ee1368194bb - languageName: node - linkType: hard - -"has-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "has-proto@npm:1.0.1" - checksum: febc5b5b531de8022806ad7407935e2135f1cc9e64636c3916c6842bd7995994ca3b29871ecd7954bd35f9e2986c17b3b227880484d22259e2f8e6ce63fd383e - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": - version: 1.0.3 - resolution: "has-symbols@npm:1.0.3" - checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.0": - version: 1.0.0 - resolution: "has-tostringtag@npm:1.0.0" - dependencies: - has-symbols: ^1.0.2 - checksum: cc12eb28cb6ae22369ebaad3a8ab0799ed61270991be88f208d508076a1e99abe4198c965935ce85ea90b60c94ddda73693b0920b58e7ead048b4a391b502c1c - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 + "@nolyfill/shared": 1.0.20 + checksum: b92b5b617a90b210cb84ecffae2dc407e087b83fcbe8658f7b422b21e93ba874d807c395e2d18a2efe93b13f9430fdebd0f2aa27bf641be78a66f79a6f536578 languageName: node linkType: hard @@ -894,28 +667,6 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.0.5": - version: 1.0.5 - resolution: "internal-slot@npm:1.0.5" - dependencies: - get-intrinsic: ^1.2.0 - has: ^1.0.3 - side-channel: ^1.0.4 - checksum: 97e84046bf9e7574d0956bd98d7162313ce7057883b6db6c5c7b5e5f05688864b0978ba07610c726d15d66544ffe4b1050107d93f8a39ebc59b15d8b429b497a - languageName: node - linkType: hard - -"is-array-buffer@npm:^3.0.1, is-array-buffer@npm:^3.0.2": - version: 3.0.2 - resolution: "is-array-buffer@npm:3.0.2" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.2.0 - is-typed-array: ^1.1.10 - checksum: dcac9dda66ff17df9cabdc58214172bf41082f956eab30bb0d86bc0fab1e44b690fc8e1f855cf2481245caf4e8a5a006a982a71ddccec84032ed41f9d8da8c14 - languageName: node - linkType: hard - "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -923,32 +674,6 @@ __metadata: languageName: node linkType: hard -"is-bigint@npm:^1.0.1": - version: 1.0.4 - resolution: "is-bigint@npm:1.0.4" - dependencies: - has-bigints: ^1.0.1 - checksum: c56edfe09b1154f8668e53ebe8252b6f185ee852a50f9b41e8d921cb2bed425652049fbe438723f6cb48a63ca1aa051e948e7e401e093477c99c84eba244f666 - languageName: node - linkType: hard - -"is-boolean-object@npm:^1.1.0": - version: 1.1.2 - resolution: "is-boolean-object@npm:1.1.2" - dependencies: - call-bind: ^1.0.2 - has-tostringtag: ^1.0.0 - checksum: c03b23dbaacadc18940defb12c1c0e3aaece7553ef58b162a0f6bba0c2a7e1551b59f365b91e00d2dbac0522392d576ef322628cb1d036a0fe51eb466db67222 - languageName: node - linkType: hard - -"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": - version: 1.2.7 - resolution: "is-callable@npm:1.2.7" - checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac - languageName: node - linkType: hard - "is-core-module@npm:^2.13.0": version: 2.13.0 resolution: "is-core-module@npm:2.13.0" @@ -958,15 +683,6 @@ __metadata: languageName: node linkType: hard -"is-date-object@npm:^1.0.1": - version: 1.0.5 - resolution: "is-date-object@npm:1.0.5" - dependencies: - has-tostringtag: ^1.0.0 - checksum: baa9077cdf15eb7b58c79398604ca57379b2fc4cf9aa7a9b9e295278648f628c9b201400c01c5e0f7afae56507d741185730307cbe7cad3b9f90a77e5ee342fc - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -981,22 +697,6 @@ __metadata: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.2": - version: 2.0.2 - resolution: "is-negative-zero@npm:2.0.2" - checksum: f3232194c47a549da60c3d509c9a09be442507616b69454716692e37ae9f37c4dea264fb208ad0c9f3efd15a796a46b79df07c7e53c6227c32170608b809149a - languageName: node - linkType: hard - -"is-number-object@npm:^1.0.4": - version: 1.0.7 - resolution: "is-number-object@npm:1.0.7" - dependencies: - has-tostringtag: ^1.0.0 - checksum: d1e8d01bb0a7134c74649c4e62da0c6118a0bfc6771ea3c560914d52a627873e6920dd0fd0ebc0e12ad2ff4687eac4c308f7e80320b973b2c8a2c8f97a7524f7 - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -1004,25 +704,6 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.4": - version: 1.1.4 - resolution: "is-regex@npm:1.1.4" - dependencies: - call-bind: ^1.0.2 - has-tostringtag: ^1.0.0 - checksum: 362399b33535bc8f386d96c45c9feb04cf7f8b41c182f54174c1a45c9abbbe5e31290bbad09a458583ff6bf3b2048672cdb1881b13289569a7c548370856a652 - languageName: node - linkType: hard - -"is-shared-array-buffer@npm:^1.0.2": - version: 1.0.2 - resolution: "is-shared-array-buffer@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - checksum: 9508929cf14fdc1afc9d61d723c6e8d34f5e117f0bffda4d97e7a5d88c3a8681f633a74f8e3ad1fe92d5113f9b921dc5ca44356492079612f9a247efbce7032a - languageName: node - linkType: hard - "is-stream@npm:^3.0.0": version: 3.0.0 resolution: "is-stream@npm:3.0.0" @@ -1030,49 +711,6 @@ __metadata: languageName: node linkType: hard -"is-string@npm:^1.0.5, is-string@npm:^1.0.7": - version: 1.0.7 - resolution: "is-string@npm:1.0.7" - dependencies: - has-tostringtag: ^1.0.0 - checksum: 323b3d04622f78d45077cf89aab783b2f49d24dc641aa89b5ad1a72114cfeff2585efc8c12ef42466dff32bde93d839ad321b26884cf75e5a7892a938b089989 - languageName: node - linkType: hard - -"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": - version: 1.0.4 - resolution: "is-symbol@npm:1.0.4" - dependencies: - has-symbols: ^1.0.2 - checksum: 92805812ef590738d9de49d677cd17dfd486794773fb6fa0032d16452af46e9b91bb43ffe82c983570f015b37136f4b53b28b8523bfb10b0ece7a66c31a54510 - languageName: node - linkType: hard - -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.9": - version: 1.1.12 - resolution: "is-typed-array@npm:1.1.12" - dependencies: - which-typed-array: ^1.1.11 - checksum: 4c89c4a3be07186caddadf92197b17fda663a9d259ea0d44a85f171558270d36059d1c386d34a12cba22dfade5aba497ce22778e866adc9406098c8fc4771796 - languageName: node - linkType: hard - -"is-weakref@npm:^1.0.2": - version: 1.0.2 - resolution: "is-weakref@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - checksum: 95bd9a57cdcb58c63b1c401c60a474b0f45b94719c30f548c891860f051bc2231575c290a6b420c6bc6e7ed99459d424c652bd5bf9a1d5259505dc35b4bf83de - languageName: node - linkType: hard - -"isarray@npm:^2.0.5": - version: 2.0.5 - resolution: "isarray@npm:2.0.5" - checksum: bd5bbe4104438c4196ba58a54650116007fa0262eccef13a4c55b2e09a5b36b59f1e75b9fcc49883dd9d4953892e6fc007eef9e9155648ceea036e184b0f930a - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1330,32 +968,6 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.3, object-inspect@npm:^1.9.0": - version: 1.12.3 - resolution: "object-inspect@npm:1.12.3" - checksum: dabfd824d97a5f407e6d5d24810d888859f6be394d8b733a77442b277e0808860555176719c5905e765e3743a7cada6b8b0a3b85e5331c530fd418cc8ae991db - languageName: node - linkType: hard - -"object-keys@npm:^1.1.1": - version: 1.1.1 - resolution: "object-keys@npm:1.1.1" - checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a - languageName: node - linkType: hard - -"object.assign@npm:^4.1.4": - version: 4.1.4 - resolution: "object.assign@npm:4.1.4" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - has-symbols: ^1.0.3 - object-keys: ^1.1.1 - checksum: 76cab513a5999acbfe0ff355f15a6a125e71805fcf53de4e9d4e082e1989bdb81d1e329291e1e4e0ae7719f0e4ef80e88fb2d367ae60500d79d25a6224ac8864 - languageName: node - linkType: hard - "once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -1524,17 +1136,6 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.0": - version: 1.5.0 - resolution: "regexp.prototype.flags@npm:1.5.0" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.2.0 - functions-have-names: ^1.2.3 - checksum: c541687cdbdfff1b9a07f6e44879f82c66bbf07665f9a7544c5fd16acdb3ec8d1436caab01662d2fbcad403f3499d49ab0b77fbc7ef29ef961d98cc4bc9755b4 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -1596,29 +1197,6 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.0.0": - version: 1.0.0 - resolution: "safe-array-concat@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.2.0 - has-symbols: ^1.0.3 - isarray: ^2.0.5 - checksum: f43cb98fe3b566327d0c09284de2b15fb85ae964a89495c1b1a5d50c7c8ed484190f4e5e71aacc167e16231940079b326f2c0807aea633d47cc7322f40a6b57f - languageName: node - linkType: hard - -"safe-regex-test@npm:^1.0.0": - version: 1.0.0 - resolution: "safe-regex-test@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.1.3 - is-regex: ^1.1.4 - checksum: bc566d8beb8b43c01b94e67de3f070fd2781685e835959bbbaaec91cc53381145ca91f69bd837ce6ec244817afa0a5e974fc4e40a2957f0aca68ac3add1ddd34 - languageName: node - linkType: hard - "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -1678,17 +1256,6 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4": - version: 1.0.4 - resolution: "side-channel@npm:1.0.4" - dependencies: - call-bind: ^1.0.0 - get-intrinsic: ^1.0.2 - object-inspect: ^1.9.0 - checksum: 351e41b947079c10bd0858364f32bb3a7379514c399edb64ab3dce683933483fc63fb5e4efe0a15a2e8a7e3c436b6a91736ddb8d8c6591b0460a24bb4a1ee245 - languageName: node - linkType: hard - "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -1776,47 +1343,12 @@ __metadata: languageName: node linkType: hard -"string.prototype.padend@npm:^3.0.0": - version: 3.1.4 - resolution: "string.prototype.padend@npm:3.1.4" +"string.prototype.padend@npm:@nolyfill/string.prototype.padend@latest": + version: 1.0.20 + resolution: "@nolyfill/string.prototype.padend@npm:1.0.20" dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 76e07238fe31dc12177428f0436b7ed6985f6a7ba97470fd53e4f0a6d9860bfee127d81957f3073cc879b434233df143825d140581e1340278053ad993c92f6c - languageName: node - linkType: hard - -"string.prototype.trim@npm:^1.2.7": - version: 1.2.7 - resolution: "string.prototype.trim@npm:1.2.7" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 05b7b2d6af63648e70e44c4a8d10d8cc457536df78b55b9d6230918bde75c5987f6b8604438c4c8652eb55e4fc9725d2912789eb4ec457d6995f3495af190c09 - languageName: node - linkType: hard - -"string.prototype.trimend@npm:^1.0.6": - version: 1.0.6 - resolution: "string.prototype.trimend@npm:1.0.6" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 0fdc34645a639bd35179b5a08227a353b88dc089adf438f46be8a7c197fc3f22f8514c1c9be4629b3cd29c281582730a8cbbad6466c60f76b5f99cf2addb132e - languageName: node - linkType: hard - -"string.prototype.trimstart@npm:^1.0.6": - version: 1.0.6 - resolution: "string.prototype.trimstart@npm:1.0.6" - dependencies: - call-bind: ^1.0.2 - define-properties: ^1.1.4 - es-abstract: ^1.20.4 - checksum: 89080feef416621e6ef1279588994305477a7a91648d9436490d56010a1f7adc39167cddac7ce0b9884b8cdbef086987c4dcb2960209f2af8bac0d23ceff4f41 + "@nolyfill/shared": 1.0.20 + checksum: 4f7853e46d8ec4d295f741d2139eeb8bddc7d64a9016abe89d9e74cd1c96252a7948831afba16afed64ac67d3eb5b3affc2e1ebcb6d871cdf4967e9eba7f2e50 languageName: node linkType: hard @@ -1942,53 +1474,6 @@ __metadata: languageName: node linkType: hard -"typed-array-buffer@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-buffer@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - get-intrinsic: ^1.2.1 - is-typed-array: ^1.1.10 - checksum: 3e0281c79b2a40cd97fe715db803884301993f4e8c18e8d79d75fd18f796e8cd203310fec8c7fdb5e6c09bedf0af4f6ab8b75eb3d3a85da69328f28a80456bd3 - languageName: node - linkType: hard - -"typed-array-byte-length@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-byte-length@npm:1.0.0" - dependencies: - call-bind: ^1.0.2 - for-each: ^0.3.3 - has-proto: ^1.0.1 - is-typed-array: ^1.1.10 - checksum: b03db16458322b263d87a702ff25388293f1356326c8a678d7515767ef563ef80e1e67ce648b821ec13178dd628eb2afdc19f97001ceae7a31acf674c849af94 - languageName: node - linkType: hard - -"typed-array-byte-offset@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-byte-offset@npm:1.0.0" - dependencies: - available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 - for-each: ^0.3.3 - has-proto: ^1.0.1 - is-typed-array: ^1.1.10 - checksum: 04f6f02d0e9a948a95fbfe0d5a70b002191fae0b8fe0fe3130a9b2336f043daf7a3dda56a31333c35a067a97e13f539949ab261ca0f3692c41603a46a94e960b - languageName: node - linkType: hard - -"typed-array-length@npm:^1.0.4": - version: 1.0.4 - resolution: "typed-array-length@npm:1.0.4" - dependencies: - call-bind: ^1.0.2 - for-each: ^0.3.3 - is-typed-array: ^1.1.9 - checksum: 2228febc93c7feff142b8c96a58d4a0d7623ecde6c7a24b2b98eb3170e99f7c7eff8c114f9b283085cd59dcd2bd43aadf20e25bba4b034a53c5bb292f71f8956 - languageName: node - linkType: hard - "typescript@npm:^5.1.6": version: 5.1.6 resolution: "typescript@npm:5.1.6" @@ -2009,18 +1494,6 @@ __metadata: languageName: node linkType: hard -"unbox-primitive@npm:^1.0.2": - version: 1.0.2 - resolution: "unbox-primitive@npm:1.0.2" - dependencies: - call-bind: ^1.0.2 - has-bigints: ^1.0.2 - has-symbols: ^1.0.3 - which-boxed-primitive: ^1.0.2 - checksum: b7a1cf5862b5e4b5deb091672ffa579aa274f648410009c81cca63fed3b62b610c4f3b773f912ce545bb4e31edc3138975b5bc777fc6e4817dca51affb6380e9 - languageName: node - linkType: hard - "uuid@npm:^9.0.0": version: 9.0.0 resolution: "uuid@npm:9.0.0" @@ -2058,32 +1531,6 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.0.2": - version: 1.0.2 - resolution: "which-boxed-primitive@npm:1.0.2" - dependencies: - is-bigint: ^1.0.1 - is-boolean-object: ^1.1.0 - is-number-object: ^1.0.4 - is-string: ^1.0.5 - is-symbol: ^1.0.3 - checksum: 53ce774c7379071729533922adcca47220228405e1895f26673bbd71bdf7fb09bee38c1d6399395927c6289476b5ae0629863427fd151491b71c4b6cb04f3a5e - languageName: node - linkType: hard - -"which-typed-array@npm:^1.1.10, which-typed-array@npm:^1.1.11": - version: 1.1.11 - resolution: "which-typed-array@npm:1.1.11" - dependencies: - available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 - for-each: ^0.3.3 - gopd: ^1.0.1 - has-tostringtag: ^1.0.0 - checksum: 711ffc8ef891ca6597b19539075ec3e08bb9b4c2ca1f78887e3c07a977ab91ac1421940505a197758fb5939aa9524976d0a5bbcac34d07ed6faa75cedbb17206 - languageName: node - linkType: hard - "which@npm:^1.2.9": version: 1.3.1 resolution: "which@npm:1.3.1"