0
2
server: Jetstream feeds the index, resuming where it was read to
Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.
crates/grain/src/jetstream.rsnew+428−0
428 lines changed: not loaded until asked for.
crates/grain/src/lib.rs+1−0
| @@ -10,6 +10,7 @@ pub mod crypto; | ||
pub mod drisl; | ||
pub mod env; | ||
pub mod identity; | ||
pub mod jetstream; | ||
pub mod keys; | ||
pub mod lexicon; | ||
pub mod oauth; | ||
crates/grain/Cargo.toml+1−0
| @@ -21,6 +21,7 @@ serde.workspace = true | ||
serde_json.workspace = true | ||
sha2.workspace = true | ||
tokio.workspace = true | ||
tokio-tungstenite.workspace = true | ||
tracing.workspace = true | ||
unicode-segmentation.workspace = true | ||
zstd.workspace = true | ||
crates/grain-serve/src/index/mod.rs+8−0
| @@ -439,6 +439,14 @@ impl Index { | ||
Ok(()) | ||
} | ||
| ||
/// Drops where the stream `source` was read to, so that it is next | ||
/// read from its live tip. | ||
pub fn forget_cursor(&self, source: &str) -> rusqlite::Result<()> { | ||
let db = self.db.lock().unwrap(); | ||
db.execute("DELETE FROM stream_cursor WHERE source = ?1", [source])?; | ||
Ok(()) | ||
} | ||
| ||
/// Gives out `label`; its sequence number. | ||
pub fn label(&self, label: &Label) -> rusqlite::Result<i64> { | ||
let db = self.db.lock().unwrap(); | ||
crates/grain-serve/src/index/tests.rs+3−0
| @@ -374,6 +374,9 @@ fn accounts() { | ||
index.set_cursor(source, 41).unwrap(); | ||
index.set_cursor(source, 42).unwrap(); | ||
assert_eq!(index.cursor(source).unwrap(), Some(42)); | ||
index.forget_cursor(source).unwrap(); | ||
assert_eq!(index.cursor(source).unwrap(), None); | ||
index.set_cursor(source, 42).unwrap(); | ||
// An account's label is on its bare DID; a record's on its URI. | ||
let label = |uri: &str, neg: bool| Label { | ||
src: "did:plc:grain".into(), | ||
crates/grain-serve/src/jetstream.rsnew+192−0
| @@ -0,0 +1,192 @@ | ||
//! The index fed from Jetstream: commits to the collections it keeps, by | ||
//! the accounts it tracks, and what hosts say of those accounts. | ||
//! | ||
//! As hatk's indexer: an account is tracked once it writes a grain profile | ||
//! or a group declaration (the signal collections), and the stream's other | ||
//! records are kept only of tracked accounts, so the Bluesky posts of the | ||
//! whole network pass by. A record that does not fit its lexicon is | ||
//! skipped, not fatal. So is an event the index fails to apply: it is | ||
//! logged and the stream goes on past it, and once the cursor is saved | ||
//! that event is not heard again. | ||
//! | ||
//! A commit is written at its revision ([`Index::put_rev`]), so that a | ||
//! stream write and a backfill's read of the same repo leave the later of | ||
//! the two. An account newly tracked, or back after its host said it was | ||
//! gone, is queued for backfill ([`Index::resync`]) to be read whole. | ||
//! | ||
//! Where it was read to is kept in the index, under `jetstream:<url>` as | ||
//! hatk's `_cursor` kept it, and a restart resumes there. A cursor | ||
//! Jetstream no longer keeps is given up for the live tip | ||
//! ([`grain::jetstream::PROBE_EVERY`]). | ||
| ||
use crate::backfill::SIGNALS; | ||
use crate::index::{Index, PutError, records}; | ||
use grain::jetstream::{Event, Follower, Timing, What}; | ||
use std::sync::Arc; | ||
use std::time::{Duration, Instant}; | ||
| ||
/// The Jetstream grain follows unless told another. | ||
pub const DEFAULT: &str = "wss://jetstream.us-west.bsky.network"; | ||
| ||
/// How often where the stream was read to is saved, at most. | ||
pub const SAVE: Duration = Duration::from_secs(5); | ||
| ||
/// The cursor's key in the index for the Jetstream at `url`: its own, as | ||
/// one instance's sequence numbers mean nothing on another. | ||
pub fn source(url: &str) -> String { | ||
format!("jetstream:{url}") | ||
} | ||
| ||
/// Follows the Jetstream at `url` into `index` until the task is dropped, | ||
/// saving where it was read to at most every `save`. | ||
pub async fn follow(index: Arc<Index>, url: String, timing: Timing, save: Duration) { | ||
let collections = records::COLLECTIONS.iter().map(|(nsid, _)| *nsid).collect(); | ||
let feed = Feed { | ||
source: source(&url), | ||
index, | ||
seq: None, | ||
saved: None, | ||
save, | ||
}; | ||
grain::jetstream::follow(url, collections, timing, feed).await | ||
} | ||
| ||
struct Feed { | ||
index: Arc<Index>, | ||
source: String, | ||
/// The last sequence number heard. | ||
seq: Option<i64>, | ||
saved: Option<Instant>, | ||
save: Duration, | ||
} | ||
| ||
impl Feed { | ||
fn save(&mut self) -> rusqlite::Result<()> { | ||
if let Some(seq) = self.seq { | ||
self.index.set_cursor(&self.source, seq)?; | ||
self.saved = Some(Instant::now()); | ||
} | ||
Ok(()) | ||
} | ||
} | ||
| ||
/// Logs an index that failed: the stream goes on. | ||
fn logged(done: rusqlite::Result<()>) { | ||
if let Err(e) = done { | ||
tracing::error!(error = %e, "jetstream: the index failed"); | ||
} | ||
} | ||
| ||
impl Follower for Feed { | ||
fn start(&mut self) -> Option<i64> { | ||
self.seq = self.index.cursor(&self.source).ok().flatten(); | ||
tracing::info!(cursor = ?self.seq, source = %self.source, "jetstream: following"); | ||
self.seq | ||
} | ||
| ||
fn connected(&mut self, up: bool) { | ||
tracing::info!(up, source = %self.source, "jetstream: connecting"); | ||
} | ||
| ||
fn event(&mut self, event: Event) { | ||
self.seq = event.seq.or(self.seq); | ||
logged(apply(&self.index, event)); | ||
if self.saved.is_none_or(|at| at.elapsed() >= self.save) { | ||
logged(self.save()); | ||
} | ||
} | ||
| ||
fn ended(&mut self) { | ||
logged(self.save()); | ||
} | ||
| ||
fn cursor_lost(&mut self, cursor: i64) { | ||
self.seq = None; | ||
logged(self.index.forget_cursor(&self.source)); | ||
tracing::warn!(cursor, source = %self.source, | ||
"jetstream: the cursor is past what Jetstream keeps; following from the live tip, \ | ||
and what came between is not heard"); | ||
} | ||
} | ||
| ||
/// What `event` says, applied to `index`. | ||
pub fn apply(index: &Index, event: Event) -> rusqlite::Result<()> { | ||
match event.what { | ||
What::Commit { did, op } => { | ||
if records::table_of(&op.collection).is_none() { | ||
return Ok(()); | ||
} | ||
// An account newly tracked is read whole by backfill. | ||
if SIGNALS.contains(&op.collection.as_str()) && index.track(&did)? { | ||
index.resync(&did)?; | ||
} | ||
if !index.tracks(&did) { | ||
return Ok(()); | ||
} | ||
let uri = format!("at://{did}/{}/{}", op.collection, op.rkey); | ||
// Written at the commit's revision, so that neither this nor a | ||
// backfill's read of the repo undoes a later one. | ||
let rev = op.rev.as_deref(); | ||
match (op.action.as_str(), op.cid, op.record) { | ||
("delete", _, _) => { | ||
match rev { | ||
Some(rev) => index.delete_rev(&uri, rev)?, | ||
None => index.delete(&uri)?, | ||
}; | ||
} | ||
("create" | "update", Some(cid), Some(record)) => { | ||
let put = match rev { | ||
Some(rev) => index.put_rev(&uri, &cid, &record, rev), | ||
None => index.put(&uri, &cid, &record), | ||
}; | ||
match put { | ||
Ok(_) => {} | ||
Err(PutError::Refused(why)) => { | ||
tracing::info!(%uri, %why, "jetstream: skipped a record"); | ||
} | ||
Err(PutError::Db(e)) => return Err(e), | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
What::Identity { did, handle } => { | ||
if let Some(handle) = handle.filter(|_| index.tracks(&did)) { | ||
index.set_handle(&did, &handle)?; | ||
} | ||
} | ||
What::Account { | ||
did, | ||
active, | ||
status, | ||
} if index.tracks(&did) => match (active, status.as_deref()) { | ||
// Back from being gone, it may have changed while it was: it is | ||
// read whole again. | ||
(true, _) => { | ||
let returned = index.host_status(&did)?.is_some(); | ||
index.set_host_status(&did, None)?; | ||
if returned { | ||
index.resync(&did)?; | ||
} | ||
} | ||
(false, Some("deleted")) => { | ||
let purged = index.purge(&did)?; | ||
index.set_host_status(&did, Some("deleted"))?; | ||
tracing::info!(%did, purged, "jetstream: an account was deleted"); | ||
} | ||
// Its host says it is not there for now: its records stay, | ||
// unread, for when it is back. | ||
(false, Some(status @ ("deactivated" | "suspended" | "takendown"))) => { | ||
index.set_host_status(&did, Some(status))?; | ||
} | ||
// `desynchronized`, `throttled` and the like say nothing of | ||
// the account itself. | ||
_ => {} | ||
}, | ||
What::Info { name, message } => { | ||
tracing::warn!(%name, message = message.unwrap_or_default(), "jetstream says"); | ||
} | ||
_ => {} | ||
} | ||
Ok(()) | ||
} | ||
crates/grain-serve/src/lib.rs+27−1
| @@ -5,9 +5,11 @@ | ||
| ||
pub mod backfill; | ||
pub mod index; | ||
pub mod jetstream; | ||
pub mod sync; | ||
pub mod xrpc; | ||
| ||
use anyhow::bail; | ||
use axum::http::StatusCode; | ||
use axum::{Json, Router, routing::any, routing::get, routing::post}; | ||
use grain::env::Ctx; | ||
| @@ -27,6 +29,10 @@ pub struct Settings { | ||
/// how to backfill the index. None, as tests and dev run, reads no | ||
/// repos. | ||
pub backfill: Option<backfill::Config>, | ||
/// `GRAIN_JETSTREAM`: the Jetstream to feed the index from, a URL, or | ||
/// `1` for [`jetstream::DEFAULT`]. None, as when it is unset, empty or | ||
/// `0`, follows none, as tests and dev run. | ||
pub jetstream: Option<String>, | ||
} | ||
| ||
impl Settings { | ||
| @@ -36,6 +42,11 @@ impl Settings { | ||
index: set("GRAIN_INDEX").map(PathBuf::from), | ||
public_url: set("GRAIN_PUBLIC_URL"), | ||
backfill: backfill::Config::from_vars(&get), | ||
jetstream: match set("GRAIN_JETSTREAM").as_deref() { | ||
None | Some("0") => None, | ||
Some("1") => Some(jetstream::DEFAULT.into()), | ||
Some(url) => Some(url.into()), | ||
}, | ||
} | ||
} | ||
| ||
| @@ -51,6 +62,21 @@ impl Settings { | ||
Ok(Some(backfill)) | ||
} | ||
| ||
/// Starts following the Jetstream these settings name into `app`'s | ||
/// index, on the runtime this is called on; whether there is one. | ||
pub fn follow(&self, app: &App) -> anyhow::Result<bool> { | ||
let Some(url) = &self.jetstream else { | ||
return Ok(false); | ||
}; | ||
let Some(index) = &app.index else { | ||
bail!("GRAIN_JETSTREAM needs GRAIN_INDEX"); | ||
}; | ||
let timing = grain::jetstream::Timing::default(); | ||
let follow = jetstream::follow(index.clone(), url.clone(), timing, jetstream::SAVE); | ||
tokio::spawn(follow); | ||
Ok(true) | ||
} | ||
| ||
/// The server `ctx` and these settings make: its index opened, and | ||
/// its DID named. | ||
pub fn app(&self, ctx: Ctx) -> anyhow::Result<App> { | ||
| @@ -173,7 +199,7 @@ mod tests { | ||
let settings = Settings { | ||
index: Some(dir.path().join("index.db")), | ||
public_url: Some("https://grain.social".into()), | ||
backfill: None, | ||
..Settings::default() | ||
}; | ||
let app = settings.app(ctx()).unwrap(); | ||
assert!(app.index.is_some()); | ||
crates/grain-serve/src/main.rs+4−2
| @@ -1,7 +1,8 @@ | ||
//! `grain-serve`: listens on `GRAIN_LISTEN`, 127.0.0.1:2600 unless set, | ||
//! keeping the index at `GRAIN_INDEX`, backfilled when `GRAIN_BACKFILL` is | ||
//! set, and taking service tokens for the DID of `GRAIN_PUBLIC_URL` when | ||
//! set ([`grain_serve::Settings`]). | ||
//! set and fed from `GRAIN_JETSTREAM` when that is, and taking service | ||
//! tokens for the DID of `GRAIN_PUBLIC_URL` when set | ||
//! ([`grain_serve::Settings`]). | ||
| ||
use anyhow::Context; | ||
use grain::env::{Ctx, Env}; | ||
| @@ -17,6 +18,7 @@ async fn main() -> anyhow::Result<()> { | ||
if let Some(backfill) = settings.backfill(&app)? { | ||
tokio::spawn(backfill.run()); | ||
} | ||
settings.follow(&app)?; | ||
let addr = std::env::var("GRAIN_LISTEN").unwrap_or_else(|_| "127.0.0.1:2600".into()); | ||
let listener = tokio::net::TcpListener::bind(&addr) | ||
.await | ||
crates/grain-serve/tests/jetstream.rsnew+647−0
647 lines changed: not loaded until asked for.
crates/grain-serve/tests/main.rs+19−1
| @@ -1,4 +1,5 @@ | ||
//! The `grain-serve` binary: it serves where `GRAIN_LISTEN` says, stops | ||
//! The `grain-serve` binary: it serves where `GRAIN_LISTEN` says, keeps its | ||
//! index where `GRAIN_INDEX` says, stops | ||
//! when told to, and says why when it cannot listen. | ||
| ||
use std::process::{Command, Stdio}; | ||
| @@ -16,8 +17,11 @@ fn free_port() -> u16 { | ||
#[tokio::test] | ||
async fn serves_until_interrupted() { | ||
let addr = format!("127.0.0.1:{}", free_port()); | ||
let dir = tempfile::tempdir().unwrap(); | ||
let index = dir.path().join("index.db"); | ||
let mut child = Command::new(env!("CARGO_BIN_EXE_grain-serve")) | ||
.env("GRAIN_LISTEN", &addr) | ||
.env("GRAIN_INDEX", &index) | ||
.stdout(Stdio::null()) | ||
.spawn() | ||
.unwrap(); | ||
| @@ -31,6 +35,7 @@ async fn serves_until_interrupted() { | ||
tokio::time::sleep(Duration::from_millis(50)).await; | ||
} | ||
assert!(up, "grain-serve never answered at {addr}"); | ||
assert!(index.exists(), "it keeps its index where GRAIN_INDEX says"); | ||
// As ctrl-c stops it: gracefully, so it exits 0. | ||
let interrupted = Command::new("kill") | ||
.args(["-INT", &child.id().to_string()]) | ||
| @@ -52,3 +57,16 @@ fn says_why_it_cannot_listen() { | ||
let said = String::from_utf8_lossy(&out.stderr); | ||
assert!(said.contains(&format!("listening on {addr}")), "{said}"); | ||
} | ||
| ||
#[test] | ||
fn says_it_cannot_follow_jetstream_without_an_index() { | ||
let out = Command::new(env!("CARGO_BIN_EXE_grain-serve")) | ||
.env("GRAIN_LISTEN", format!("127.0.0.1:{}", free_port())) | ||
.env("GRAIN_JETSTREAM", "1") | ||
.env_remove("GRAIN_INDEX") | ||
.output() | ||
.unwrap(); | ||
assert!(!out.status.success()); | ||
let said = String::from_utf8_lossy(&out.stderr); | ||
assert!(said.contains("GRAIN_JETSTREAM needs GRAIN_INDEX"), "{said}"); | ||
} | ||
crates/grain-serve/Cargo.toml+1−0
| @@ -22,4 +22,5 @@ tracing.workspace = true | ||
tracing-subscriber.workspace = true | ||
| ||
[dev-dependencies] | ||
tokio-tungstenite.workspace = true | ||
zstd.workspace = true | ||
Cargo.lock+137−4
| @@ -293,6 +293,12 @@ dependencies = [ | ||
"typenum", | ||
] | ||
| ||
[[package]] | ||
name = "data-encoding" | ||
version = "2.11.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" | ||
| ||
[[package]] | ||
name = "der" | ||
version = "0.7.10" | ||
| @@ -555,6 +561,18 @@ dependencies = [ | ||
"wasm-bindgen", | ||
] | ||
| ||
[[package]] | ||
name = "getrandom" | ||
version = "0.3.4" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | ||
dependencies = [ | ||
"cfg-if", | ||
"libc", | ||
"r-efi 5.3.0", | ||
"wasip2", | ||
] | ||
| ||
[[package]] | ||
name = "getrandom" | ||
version = "0.4.3" | ||
| @@ -564,7 +582,7 @@ dependencies = [ | ||
"cfg-if", | ||
"js-sys", | ||
"libc", | ||
"r-efi", | ||
"r-efi 6.0.0", | ||
"rand_core 0.10.1", | ||
"wasm-bindgen", | ||
] | ||
| @@ -589,6 +607,7 @@ dependencies = [ | ||
"sha2", | ||
"tempfile", | ||
"tokio", | ||
"tokio-tungstenite", | ||
"tracing", | ||
"tracing-subscriber", | ||
"unicode-segmentation", | ||
| @@ -610,6 +629,7 @@ dependencies = [ | ||
"serde_json", | ||
"tempfile", | ||
"tokio", | ||
"tokio-tungstenite", | ||
"tracing", | ||
"tracing-subscriber", | ||
"zstd", | ||
| @@ -772,7 +792,7 @@ dependencies = [ | ||
"tokio", | ||
"tokio-rustls", | ||
"tower-service", | ||
"webpki-roots", | ||
"webpki-roots 1.0.11", | ||
] | ||
| ||
[[package]] | ||
| @@ -1119,6 +1139,15 @@ dependencies = [ | ||
"zerovec", | ||
] | ||
| ||
[[package]] | ||
name = "ppv-lite86" | ||
version = "0.2.21" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||
dependencies = [ | ||
"zerocopy", | ||
] | ||
| ||
[[package]] | ||
name = "primeorder" | ||
version = "0.13.6" | ||
| @@ -1166,7 +1195,7 @@ dependencies = [ | ||
"bytes", | ||
"getrandom 0.4.3", | ||
"lru-slab", | ||
"rand", | ||
"rand 0.10.3", | ||
"rand_pcg", | ||
"ring", | ||
"rustc-hash", | ||
| @@ -1202,12 +1231,28 @@ dependencies = [ | ||
"proc-macro2", | ||
] | ||
| ||
[[package]] | ||
name = "r-efi" | ||
version = "5.3.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | ||
| ||
[[package]] | ||
name = "r-efi" | ||
version = "6.0.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | ||
| ||
[[package]] | ||
name = "rand" | ||
version = "0.9.5" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" | ||
dependencies = [ | ||
"rand_chacha", | ||
"rand_core 0.9.5", | ||
] | ||
| ||
[[package]] | ||
name = "rand" | ||
version = "0.10.3" | ||
| @@ -1219,6 +1264,16 @@ dependencies = [ | ||
"rand_core 0.10.1", | ||
] | ||
| ||
[[package]] | ||
name = "rand_chacha" | ||
version = "0.9.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | ||
dependencies = [ | ||
"ppv-lite86", | ||
"rand_core 0.9.5", | ||
] | ||
| ||
[[package]] | ||
name = "rand_core" | ||
version = "0.6.4" | ||
| @@ -1228,6 +1283,15 @@ dependencies = [ | ||
"getrandom 0.2.17", | ||
] | ||
| ||
[[package]] | ||
name = "rand_core" | ||
version = "0.9.5" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" | ||
dependencies = [ | ||
"getrandom 0.3.4", | ||
] | ||
| ||
[[package]] | ||
name = "rand_core" | ||
version = "0.10.1" | ||
| @@ -1296,7 +1360,7 @@ dependencies = [ | ||
"wasm-bindgen", | ||
"wasm-bindgen-futures", | ||
"web-sys", | ||
"webpki-roots", | ||
"webpki-roots 1.0.11", | ||
] | ||
| ||
[[package]] | ||
| @@ -1494,6 +1558,17 @@ dependencies = [ | ||
"serde", | ||
] | ||
| ||
[[package]] | ||
name = "sha1" | ||
version = "0.10.7" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" | ||
dependencies = [ | ||
"cfg-if", | ||
"cpufeatures 0.2.17", | ||
"digest", | ||
] | ||
| ||
[[package]] | ||
name = "sha2" | ||
version = "0.10.9" | ||
| @@ -1739,6 +1814,22 @@ dependencies = [ | ||
"tokio", | ||
] | ||
| ||
[[package]] | ||
name = "tokio-tungstenite" | ||
version = "0.29.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" | ||
dependencies = [ | ||
"futures-util", | ||
"log", | ||
"rustls", | ||
"rustls-pki-types", | ||
"tokio", | ||
"tokio-rustls", | ||
"tungstenite", | ||
"webpki-roots 0.26.11", | ||
] | ||
| ||
[[package]] | ||
name = "tokio-util" | ||
version = "0.7.20" | ||
| @@ -1871,6 +1962,24 @@ version = "0.2.5" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | ||
| ||
[[package]] | ||
name = "tungstenite" | ||
version = "0.29.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" | ||
dependencies = [ | ||
"bytes", | ||
"data-encoding", | ||
"http", | ||
"httparse", | ||
"log", | ||
"rand 0.9.5", | ||
"rustls", | ||
"rustls-pki-types", | ||
"sha1", | ||
"thiserror", | ||
] | ||
| ||
[[package]] | ||
name = "typenum" | ||
version = "1.20.1" | ||
| @@ -1955,6 +2064,15 @@ version = "0.11.1+wasi-snapshot-preview1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | ||
| ||
[[package]] | ||
name = "wasip2" | ||
version = "1.0.4+wasi-0.2.12" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" | ||
dependencies = [ | ||
"wit-bindgen", | ||
] | ||
| ||
[[package]] | ||
name = "wasm-bindgen" | ||
version = "0.2.130" | ||
| @@ -2030,6 +2148,15 @@ dependencies = [ | ||
"wasm-bindgen", | ||
] | ||
| ||
[[package]] | ||
name = "webpki-roots" | ||
version = "0.26.11" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" | ||
dependencies = [ | ||
"webpki-roots 1.0.11", | ||
] | ||
| ||
[[package]] | ||
name = "webpki-roots" | ||
version = "1.0.11" | ||
| @@ -2127,6 +2254,12 @@ version = "0.52.6" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||
| ||
[[package]] | ||
name = "wit-bindgen" | ||
version = "0.57.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" | ||
| ||
[[package]] | ||
name = "writeable" | ||
version = "0.6.4" | ||