//! The Docs core: Yjs CRDT and (later) text layout, compiled to wasm and //! run both under wazero on the server and — for layout only — in the browser. //! //! # Important: Doc construction //! //! Never call `yrs::Doc::new()` *or* `yrs::Doc::with_client_id()` in this //! crate. Both draw entropy from `fastrand`, not just `Doc::new()`: //! `Doc::new()` picks a random client id *and* GUID; `Doc::with_client_id()` //! takes the client id explicitly but still draws a random GUID internally //! (`Options::with_client_id`, `yrs` `src/doc.rs:916`, calls `uuid_v4()` //! regardless). The only construction that supplies everything itself, and //! so is safe to use, is `Doc::with_options(Options::with_guid_and_client_id //! (guid, client_id))` (`doc.rs:928`) with both values we choose. This is //! enforced by `clippy.toml` (`disallowed-methods`), wired into //! `scripts/build-wasm.sh` — not just documented here, because this rule //! has been gotten wrong more than once already (see below). //! //! # Zero host imports and the custom getrandom backend //! //! On `wasm32-unknown-unknown`, `yrs`'s pinned `fastrand` dependency //! requires the `js` feature, which makes the transitively-pulled //! `getrandom` crate default to a `wasm-bindgen`-based backend that imports //! host JS functions. Merely referencing `yrs::Doc` anywhere in this crate //! is enough for those `wasm-bindgen` exports (`__wbindgen_describe`, //! `__wbindgen_malloc`, ...) to end up linked into the final module, even //! along code paths that are never reached at runtime — which breaks the //! zero-imports contract with the Go/wazero host (see //! `core/tests/no_host_imports.rs`). //! //! `docs-wt-core/.cargo/config.toml` selects getrandom's "custom" backend //! for the `wasm32-unknown-unknown` target only (native builds, including //! `cargo test`, are unaffected and keep the real OS entropy source). That //! backend calls out to the `__getrandom_v03_custom` symbol defined below //! instead of any JS API. It refuses the request (`Err`) rather than //! fabricating bytes — but that refusal is weaker than it sounds, and the //! next paragraph is the actual reason construction must never draw a GUID //! or client id from the library, not "so a panic tells us": //! //! `fastrand::Rng::new()` — what both `Observer::subscribe()` //! (`yrs` `src/observer.rs:128`/`:145`, subscription ids) and `uuid_v4()` //! (`yrs` `src/lib.rs:713`, document GUIDs) call — does not propagate our //! `Err`. It masks it: `vendor/fastrand-2.5.0/src/global_rng.rs:223` turns //! any `getrandom` failure into `None` and falls back to a constant seed. //! `Rng::new()` itself then *forks* the thread-local generator from that //! seed and advances it, so successive calls within one process still //! produce different values — subscription ids don't collide with each //! other, and this crate's own tests (which never construct more than one //! `Doc` per process) can't see anything wrong. The actual hazard is //! cross-process: the thread-local seed is the same fixed constant on //! every process, every time, so the *sequence* of "random" ids/GUIDs a //! process draws is identical across processes. Two independently-created //! documents that never draw a client id or GUID explicitly would collide. //! This crate avoids the whole problem by supplying both explicitly (see //! above) rather than relying on `fastrand` to fail loudly when it can't — //! it won't. pub mod abi; mod fragment; mod snapshot; pub use fragment::{fragment_html, fragment_json, fragment_text, ROOT as FRAGMENT_ROOT}; pub use snapshot::{snapshot_bytes, verify_snapshot}; #[cfg(target_arch = "wasm32")] #[no_mangle] extern "Rust" fn __getrandom_v03_custom( _dest: *mut u8, _len: usize, ) -> Result<(), getrandom::Error> { Err(getrandom::Error::UNSUPPORTED) } /// Returns the ABI version this module exports. The Go host checks this on /// load and refuses a module it does not understand, so a stale .wasm on a /// box fails loudly instead of behaving strangely. /// /// Bumped to 2 for the HTML read pair in `abi.rs` /// (`docs_core_html_len` / `docs_core_html_read`), to 3 for the JSON /// pair (`docs_core_json_len` / `docs_core_json_read`) the export paths /// read the document's structure through, and to 4 for the snapshot /// pair (`docs_core_snapshot_len` / `docs_core_snapshot_read`) that /// exports a document's whole state as one update. `docs_core_verify_snapshot`, /// which checks a candidate snapshot against a live document, lands /// within that same version 4 rather than needing one of its own — /// both pairs were added before version 4 ever shipped, so no host in /// the field has seen a 4 that lacks this export. That is the only /// reason a shared number is safe here: an export added after a /// version has already shipped forces its own bump regardless, for the /// reason the next paragraph gives. /// /// Adding an export is, strictly, backwards compatible — a host that /// never calls the new function cannot tell 3 from 2 — so the bump is /// not forced by the wasm itself. It is forced by the host: the module /// and the Go binary ship embedded in each other with no version skew /// between separately deployed copies, and `internal/core`'s /// `abiVersion` is an exact-match check. A host that calls /// `docs_core_json_len` against a stale module gets "unknown export" at /// call time, deep inside a request; bumping means it gets a refusal at /// load time instead, which is the whole point of there being a version /// here. #[no_mangle] pub extern "C" fn docs_core_abi_version() -> u32 { 4 } use yrs::updates::decoder::Decode; use yrs::{Doc, GetString, Transact, Update}; #[derive(Debug)] pub enum CoreError { /// The bytes were not a valid Yjs v1 update. Decode(String), /// The update decoded but could not be applied. Apply(String), } impl std::fmt::Display for CoreError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CoreError::Decode(m) => write!(f, "decode: {m}"), CoreError::Apply(m) => write!(f, "apply: {m}"), } } } /// Applies a Yjs v1 update to `doc`. pub fn apply_update(doc: &Doc, update: &[u8]) -> Result<(), CoreError> { let update = Update::decode_v1(update).map_err(|e| CoreError::Decode(e.to_string()))?; let mut txn = doc.transact_mut(); txn.apply_update(update) .map_err(|e| CoreError::Apply(e.to_string())) } /// The plain-text content of the document's root text type. The name /// "content" matches what the fixture generator writes; it is the CRDT root /// name and is part of the wire contract. pub fn text_content(doc: &Doc) -> String { let text = doc.get_or_insert_text("content"); let txn = doc.transact(); text.get_string(&txn) }